From d0cc13ce0b2a149c3d19aa58a2f12ddc6cc0196f Mon Sep 17 00:00:00 2001
From: MutantAura <44103205+MutantAura@users.noreply.github.com>
Date: Wed, 1 May 2024 17:21:24 +0100
Subject: [PATCH 001/109] UI: Fix some MainWindow bugs and implement menubar
items to change window size. (#6750)
* Do not save window dimensions when maximized.
* Implement option to disable window size/position memory.
* Remove logging statements
* Implement menubar items for common window sizes.
* formatting fixes
* Set 720p window as a composite value.
* Remove unused using
* Fix exception paramter.
* Force restore window when a size change is requested
* Fix some resizing bugs.
---
.../Configuration/ConfigurationFileFormat.cs | 7 +++-
.../Configuration/ConfigurationState.cs | 18 ++++++++++
src/Ryujinx/Assets/Locales/en_US.json | 5 +++
.../UI/ViewModels/SettingsViewModel.cs | 3 ++
.../UI/Views/Main/MainMenuBarView.axaml | 6 ++++
.../UI/Views/Main/MainMenuBarView.axaml.cs | 35 ++++++++++++++++++
.../UI/Views/Settings/SettingsUIView.axaml | 3 ++
src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 36 ++++++++++++++-----
8 files changed, 103 insertions(+), 10 deletions(-)
diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs
index 3387e1be1..af3ad0a1d 100644
--- a/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs
+++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationFileFormat.cs
@@ -15,7 +15,7 @@ namespace Ryujinx.UI.Common.Configuration
///
/// The current version of the file format
///
- public const int CurrentVersion = 50;
+ public const int CurrentVersion = 51;
///
/// Version of the configuration file format
@@ -162,6 +162,11 @@ namespace Ryujinx.UI.Common.Configuration
///
public bool ShowConfirmExit { get; set; }
+ ///
+ /// Enables or disables save window size, position and state on close.
+ ///
+ public bool RememberWindowState { get; set; }
+
///
/// Enables hardware-accelerated rendering for Avalonia
///
diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs
index 2609dc9ba..01c60a5e2 100644
--- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs
+++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs
@@ -626,6 +626,11 @@ namespace Ryujinx.UI.Common.Configuration
///
public ReactiveObject ShowConfirmExit { get; private set; }
+ ///
+ /// Enables or disables save window size, position and state on close.
+ ///
+ public ReactiveObject RememberWindowState { get; private set; }
+
///
/// Enables hardware-accelerated rendering for Avalonia
///
@@ -647,6 +652,7 @@ namespace Ryujinx.UI.Common.Configuration
EnableDiscordIntegration = new ReactiveObject();
CheckUpdatesOnStart = new ReactiveObject();
ShowConfirmExit = new ReactiveObject();
+ RememberWindowState = new ReactiveObject();
EnableHardwareAcceleration = new ReactiveObject();
HideCursor = new ReactiveObject();
}
@@ -684,6 +690,7 @@ namespace Ryujinx.UI.Common.Configuration
EnableDiscordIntegration = EnableDiscordIntegration,
CheckUpdatesOnStart = CheckUpdatesOnStart,
ShowConfirmExit = ShowConfirmExit,
+ RememberWindowState = RememberWindowState,
EnableHardwareAcceleration = EnableHardwareAcceleration,
HideCursor = HideCursor,
EnableVsync = Graphics.EnableVsync,
@@ -792,6 +799,7 @@ namespace Ryujinx.UI.Common.Configuration
EnableDiscordIntegration.Value = true;
CheckUpdatesOnStart.Value = true;
ShowConfirmExit.Value = true;
+ RememberWindowState.Value = true;
EnableHardwareAcceleration.Value = true;
HideCursor.Value = HideCursorMode.OnIdle;
Graphics.EnableVsync.Value = true;
@@ -1459,6 +1467,15 @@ namespace Ryujinx.UI.Common.Configuration
configurationFileUpdated = true;
}
+ if (configurationFileFormat.Version < 51)
+ {
+ Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 51.");
+
+ configurationFileFormat.RememberWindowState = true;
+
+ configurationFileUpdated = true;
+ }
+
Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog;
Graphics.ResScale.Value = configurationFileFormat.ResScale;
Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom;
@@ -1489,6 +1506,7 @@ namespace Ryujinx.UI.Common.Configuration
EnableDiscordIntegration.Value = configurationFileFormat.EnableDiscordIntegration;
CheckUpdatesOnStart.Value = configurationFileFormat.CheckUpdatesOnStart;
ShowConfirmExit.Value = configurationFileFormat.ShowConfirmExit;
+ RememberWindowState.Value = configurationFileFormat.RememberWindowState;
EnableHardwareAcceleration.Value = configurationFileFormat.EnableHardwareAcceleration;
HideCursor.Value = configurationFileFormat.HideCursor;
Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync;
diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json
index 77ad7d1f8..2299d9e66 100644
--- a/src/Ryujinx/Assets/Locales/en_US.json
+++ b/src/Ryujinx/Assets/Locales/en_US.json
@@ -30,6 +30,10 @@
"MenuBarToolsManageFileTypes": "Manage file types",
"MenuBarToolsInstallFileTypes": "Install file types",
"MenuBarToolsUninstallFileTypes": "Uninstall file types",
+ "MenuBarView": "_View",
+ "MenuBarViewWindow": "Window Size",
+ "MenuBarViewWindow720": "720p",
+ "MenuBarViewWindow1080": "1080p",
"MenuBarHelp": "_Help",
"MenuBarHelpCheckForUpdates": "Check for Updates",
"MenuBarHelpAbout": "About",
@@ -92,6 +96,7 @@
"SettingsTabGeneralEnableDiscordRichPresence": "Enable Discord Rich Presence",
"SettingsTabGeneralCheckUpdatesOnLaunch": "Check for Updates on Launch",
"SettingsTabGeneralShowConfirmExitDialog": "Show \"Confirm Exit\" Dialog",
+ "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position",
"SettingsTabGeneralHideCursor": "Hide Cursor:",
"SettingsTabGeneralHideCursorNever": "Never",
"SettingsTabGeneralHideCursorOnIdle": "On Idle",
diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
index 6074a5fdb..0664f436f 100644
--- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
@@ -131,6 +131,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool EnableDiscordIntegration { get; set; }
public bool CheckUpdatesOnStart { get; set; }
public bool ShowConfirmExit { get; set; }
+ public bool RememberWindowState { get; set; }
public int HideCursor { get; set; }
public bool EnableDockedMode { get; set; }
public bool EnableKeyboard { get; set; }
@@ -390,6 +391,7 @@ namespace Ryujinx.Ava.UI.ViewModels
EnableDiscordIntegration = config.EnableDiscordIntegration;
CheckUpdatesOnStart = config.CheckUpdatesOnStart;
ShowConfirmExit = config.ShowConfirmExit;
+ RememberWindowState = config.RememberWindowState;
HideCursor = (int)config.HideCursor.Value;
GameDirectories.Clear();
@@ -474,6 +476,7 @@ namespace Ryujinx.Ava.UI.ViewModels
config.EnableDiscordIntegration.Value = EnableDiscordIntegration;
config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart;
config.ShowConfirmExit.Value = ShowConfirmExit;
+ config.RememberWindowState.Value = RememberWindowState;
config.HideCursor.Value = (HideCursorMode)HideCursor;
if (_directoryChanged)
diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml
index ea432f78d..ac3736110 100644
--- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml
+++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml
@@ -186,6 +186,12 @@
+
+ {
+ ViewModel.WindowState = WindowState.Normal;
+
+ height += (int)Window.StatusBarHeight + (int)Window.MenuBarHeight;
+
+ Window.Arrange(new Rect(Window.Position.X, Window.Position.Y, width, height));
+ });
+ }
+ }
+
public async void CheckForUpdates(object sender, RoutedEventArgs e)
{
if (Updater.CanUpdate(true))
diff --git a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml
index 6504637e6..b60058fcb 100644
--- a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml
+++ b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml
@@ -36,6 +36,9 @@
+
+
+
Date: Thu, 2 May 2024 08:33:28 -0300
Subject: [PATCH 002/109] Fix system dateTime loading in avalonia
LoadCurrentConfiguration (#6676)
* Fix system dateTime loading in avalonia LoadCurrentConfiguration
* Rename local var to not use upper camel case
---
src/Ryujinx/UI/ViewModels/SettingsViewModel.cs | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
index 0664f436f..0f43d0f7f 100644
--- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
@@ -412,10 +412,11 @@ namespace Ryujinx.Ava.UI.ViewModels
Language = (int)config.System.Language.Value;
TimeZone = config.System.TimeZone;
- DateTime currentDateTime = DateTime.Now;
-
+ DateTime currentHostDateTime = DateTime.Now;
+ TimeSpan systemDateTimeOffset = TimeSpan.FromSeconds(config.System.SystemTimeOffset);
+ DateTime currentDateTime = currentHostDateTime.Add(systemDateTimeOffset);
CurrentDate = currentDateTime.Date;
- CurrentTime = currentDateTime.TimeOfDay.Add(TimeSpan.FromSeconds(config.System.SystemTimeOffset));
+ CurrentTime = currentDateTime.TimeOfDay;
EnableVsync = config.Graphics.EnableVsync;
EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks;
From a23d8cb92f3f1bb8dc144f4d9fb3fddee749feae Mon Sep 17 00:00:00 2001
From: Marco Carvalho
Date: Wed, 8 May 2024 08:53:25 -0300
Subject: [PATCH 003/109] Replace "List.ForEach" for "foreach" (#6783)
* Replace "List.ForEach" for "foreach"
* dotnet format
* Update Ptc.cs
* Update GpuContext.cs
---
src/ARMeilleure/Translation/PTC/Ptc.cs | 10 ++++++++--
src/Ryujinx.Graphics.Gpu/GpuContext.cs | 10 ++++++++--
2 files changed, 16 insertions(+), 4 deletions(-)
diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs
index 58f065342..f56bdce1c 100644
--- a/src/ARMeilleure/Translation/PTC/Ptc.cs
+++ b/src/ARMeilleure/Translation/PTC/Ptc.cs
@@ -857,8 +857,14 @@ namespace ARMeilleure.Translation.PTC
Stopwatch sw = Stopwatch.StartNew();
- threads.ForEach((thread) => thread.Start());
- threads.ForEach((thread) => thread.Join());
+ foreach (var thread in threads)
+ {
+ thread.Start();
+ }
+ foreach (var thread in threads)
+ {
+ thread.Join();
+ }
threads.Clear();
diff --git a/src/Ryujinx.Graphics.Gpu/GpuContext.cs b/src/Ryujinx.Graphics.Gpu/GpuContext.cs
index aa0084fdc..53ea8cb27 100644
--- a/src/Ryujinx.Graphics.Gpu/GpuContext.cs
+++ b/src/Ryujinx.Graphics.Gpu/GpuContext.cs
@@ -395,8 +395,14 @@ namespace Ryujinx.Graphics.Gpu
{
Renderer.CreateSync(SyncNumber, strict);
- SyncActions.ForEach(action => action.SyncPreAction(syncpoint));
- SyncpointActions.ForEach(action => action.SyncPreAction(syncpoint));
+ foreach (var action in SyncActions)
+ {
+ action.SyncPreAction(syncpoint);
+ }
+ foreach (var action in SyncpointActions)
+ {
+ action.SyncPreAction(syncpoint);
+ }
SyncNumber++;
From 1a676ee913ee98917ec384481c8f6666835f942f Mon Sep 17 00:00:00 2001
From: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com>
Date: Tue, 14 May 2024 09:59:28 -0400
Subject: [PATCH 004/109] Update DotSettings (#6535)
---
Ryujinx.sln.DotSettings | 2 ++
1 file changed, 2 insertions(+)
diff --git a/Ryujinx.sln.DotSettings b/Ryujinx.sln.DotSettings
index 049bdaf69..ed7f3e911 100644
--- a/Ryujinx.sln.DotSettings
+++ b/Ryujinx.sln.DotSettings
@@ -4,6 +4,8 @@
UseExplicitType
UseExplicitType
<Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy>
+ <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="I" Suffix="" Style="AaBb" /></Policy></Policy>
+ True
True
True
True
From 6e40b645547f6453fb5e0bee98d2fa6330e747c7 Mon Sep 17 00:00:00 2001
From: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
Date: Tue, 14 May 2024 16:06:40 +0200
Subject: [PATCH 005/109] Add linux specific files to local builds (#6762)
---
src/Ryujinx.Gtk3/Ryujinx.Gtk3.csproj | 2 +-
src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj | 2 +-
src/Ryujinx/Ryujinx.csproj | 5 +++--
3 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/Ryujinx.Gtk3/Ryujinx.Gtk3.csproj b/src/Ryujinx.Gtk3/Ryujinx.Gtk3.csproj
index 68bf98981..b4453f9d7 100644
--- a/src/Ryujinx.Gtk3/Ryujinx.Gtk3.csproj
+++ b/src/Ryujinx.Gtk3/Ryujinx.Gtk3.csproj
@@ -63,7 +63,7 @@
-
+
Always
diff --git a/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj
index cc5a36518..610229544 100644
--- a/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj
+++ b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj
@@ -48,7 +48,7 @@
-
+
Always
diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj
index 38453f2c6..a43f50063 100644
--- a/src/Ryujinx/Ryujinx.csproj
+++ b/src/Ryujinx/Ryujinx.csproj
@@ -70,7 +70,8 @@
-
+
@@ -88,7 +89,7 @@
-
+
Always
From e9edf0ab7fa9c64347c3e53e0d9d862756b1627f Mon Sep 17 00:00:00 2001
From: Luke Warner <65521430+LukeWarnut@users.noreply.github.com>
Date: Tue, 14 May 2024 10:19:43 -0400
Subject: [PATCH 006/109] Update outdated Windows version warning (#6481)
* Change text
* clarify version number
* update gtk
---
src/Ryujinx.Gtk3/Program.cs | 2 +-
src/Ryujinx/Program.cs | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Ryujinx.Gtk3/Program.cs b/src/Ryujinx.Gtk3/Program.cs
index 1845c512e..f69934313 100644
--- a/src/Ryujinx.Gtk3/Program.cs
+++ b/src/Ryujinx.Gtk3/Program.cs
@@ -75,7 +75,7 @@ namespace Ryujinx
if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134))
{
- MessageBoxA(IntPtr.Zero, "You are running an outdated version of Windows.\n\nStarting on June 1st 2022, Ryujinx will only support Windows 10 1803 and newer.\n", $"Ryujinx {Version}", MbIconWarning);
+ MessageBoxA(IntPtr.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 1803 and newer.\n", $"Ryujinx {Version}", MbIconWarning);
}
// Parse arguments
diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs
index 89e895e81..f925ce154 100644
--- a/src/Ryujinx/Program.cs
+++ b/src/Ryujinx/Program.cs
@@ -40,7 +40,7 @@ namespace Ryujinx.Ava
if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134))
{
- _ = MessageBoxA(IntPtr.Zero, "You are running an outdated version of Windows.\n\nStarting on June 1st 2022, Ryujinx will only support Windows 10 1803 and newer.\n", $"Ryujinx {Version}", MbIconwarning);
+ _ = MessageBoxA(IntPtr.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 1803 and newer.\n", $"Ryujinx {Version}", MbIconwarning);
}
PreviewerDetached = true;
From cada4d04efe8a3c19c290da3257c231da9872e40 Mon Sep 17 00:00:00 2001
From: Tsubasa0504 <60139445+Tsubasa0504@users.noreply.github.com>
Date: Tue, 14 May 2024 23:26:49 +0900
Subject: [PATCH 007/109] HID: Stub IHidServer: 134
(SetNpadAnalogStickUseCenterClamp) (#6664)
* Add files via upload
* Update IHidServer.cs
mistakes...
* format
how do i do it
* Update src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs
Co-authored-by: Agatem
* Update src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs
Co-authored-by: Agatem
* bruh
* Apply suggestions from code review
Co-authored-by: gdkchan
* use readuint32 instead
* second thought
* i hope it works
thanks someone higher up with the same thing
* pid
* Apply suggestions from code review
Co-authored-by: Ac_K
* styles i think
* Apply suggestions from code review
Co-authored-by: makigumo
---------
Co-authored-by: Agatem
Co-authored-by: gdkchan
Co-authored-by: Ac_K
Co-authored-by: makigumo
---
src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs b/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs
index bcc87f53b..e3f505f37 100644
--- a/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs
+++ b/src/Ryujinx.HLE/HOS/Services/Hid/IHidServer.cs
@@ -22,6 +22,7 @@ namespace Ryujinx.HLE.HOS.Services.Hid
private bool _sixAxisSensorFusionEnabled;
private bool _unintendedHomeButtonInputProtectionEnabled;
+ private bool _npadAnalogStickCenterClampEnabled;
private bool _vibrationPermitted;
private bool _usbFullKeyControllerEnabled;
private readonly bool _isFirmwareUpdateAvailableForSixAxisSensor;
@@ -1107,6 +1108,19 @@ namespace Ryujinx.HLE.HOS.Services.Hid
// If not, it returns nothing.
}
+ [CommandCmif(134)] // 6.1.0+
+ // SetNpadUseAnalogStickUseCenterClamp(bool Enable, nn::applet::AppletResourceUserId)
+ public ResultCode SetNpadUseAnalogStickUseCenterClamp(ServiceCtx context)
+ {
+ ulong pid = context.RequestData.ReadUInt64();
+ _npadAnalogStickCenterClampEnabled = context.RequestData.ReadUInt32() != 0;
+ long appletResourceUserId = context.RequestData.ReadInt64();
+
+ Logger.Stub?.PrintStub(LogClass.ServiceHid, new { pid, appletResourceUserId, _npadAnalogStickCenterClampEnabled });
+
+ return ResultCode.Success;
+ }
+
[CommandCmif(200)]
// GetVibrationDeviceInfo(nn::hid::VibrationDeviceHandle) -> nn::hid::VibrationDeviceInfo
public ResultCode GetVibrationDeviceInfo(ServiceCtx context)
From 44dbab3848c8831d27e50f7252d759a2494ad556 Mon Sep 17 00:00:00 2001
From: MutantAura <44103205+MutantAura@users.noreply.github.com>
Date: Tue, 14 May 2024 15:36:44 +0100
Subject: [PATCH 008/109] discordRPC: Truncate game title and details if they
exceed discord byte limit. (#6581)
* Truncate game title and details if they exceed DiscordRPC limit.
* Update implementation to a byte total check.
* Track if the string has actually been modified correctly.
* Allow an early function return and simplify logic.
* Better handling of large input strings and minimise loop opportunities.
* Remove unused using.
* Update to `applicationName` over `titleName`.
---
.../DiscordIntegrationModule.cs | 32 +++++++++++++++++--
1 file changed, 29 insertions(+), 3 deletions(-)
diff --git a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs
index bbece1e1d..fb07195d0 100644
--- a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs
+++ b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs
@@ -1,6 +1,7 @@
using DiscordRPC;
using Ryujinx.Common;
using Ryujinx.UI.Common.Configuration;
+using System.Text;
namespace Ryujinx.UI.Common
{
@@ -9,6 +10,9 @@ namespace Ryujinx.UI.Common
private const string Description = "A simple, experimental Nintendo Switch emulator.";
private const string ApplicationId = "1216775165866807456";
+ private const int ApplicationByteLimit = 128;
+ private const string Ellipsis = "…";
+
private static DiscordRpcClient _discordClient;
private static RichPresence _discordPresenceMain;
@@ -60,18 +64,18 @@ namespace Ryujinx.UI.Common
}
}
- public static void SwitchToPlayingState(string titleId, string titleName)
+ public static void SwitchToPlayingState(string titleId, string applicationName)
{
_discordClient?.SetPresence(new RichPresence
{
Assets = new Assets
{
LargeImageKey = "game",
- LargeImageText = titleName,
+ LargeImageText = TruncateToByteLength(applicationName, ApplicationByteLimit),
SmallImageKey = "ryujinx",
SmallImageText = Description,
},
- Details = $"Playing {titleName}",
+ Details = TruncateToByteLength($"Playing {applicationName}", ApplicationByteLimit),
State = (titleId == "0000000000000000") ? "Homebrew" : titleId.ToUpper(),
Timestamps = Timestamps.Now,
Buttons =
@@ -90,6 +94,28 @@ namespace Ryujinx.UI.Common
_discordClient?.SetPresence(_discordPresenceMain);
}
+ private static string TruncateToByteLength(string input, int byteLimit)
+ {
+ if (Encoding.UTF8.GetByteCount(input) <= byteLimit)
+ {
+ return input;
+ }
+
+ // Find the length to trim the string to guarantee we have space for the trailing ellipsis.
+ int trimLimit = byteLimit - Encoding.UTF8.GetByteCount(Ellipsis);
+
+ // Basic trim to best case scenario of 1 byte characters.
+ input = input[..trimLimit];
+
+ while (Encoding.UTF8.GetByteCount(input) > trimLimit)
+ {
+ // Remove one character from the end of the string at a time.
+ input = input[..^1];
+ }
+
+ return input.TrimEnd() + Ellipsis;
+ }
+
public static void Exit()
{
_discordClient?.Dispose();
From 3a3b51893ee272af49d762387da5b27743786d56 Mon Sep 17 00:00:00 2001
From: gdkchan
Date: Tue, 14 May 2024 11:47:16 -0300
Subject: [PATCH 009/109] Add support for bindless textures from storage buffer
on Vulkan (#6721)
* Halve primitive ID when converting quads to triangles
* Shader cache version bump
* Add support for bindless textures from storage buffer on Vulkan
---
src/Ryujinx.Graphics.GAL/Capabilities.cs | 3 +++
.../Shader/DiskCache/DiskCacheGpuAccessor.cs | 14 +++++++++++--
.../Shader/DiskCache/DiskCacheHostStorage.cs | 2 +-
.../DiskCache/ParallelDiskCacheLoader.cs | 8 +++++---
.../Shader/GpuAccessor.cs | 20 ++++++++++++++++---
.../Shader/GpuChannelGraphicsState.cs | 11 ++++++++--
.../Shader/ShaderCache.cs | 2 +-
src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs | 3 ++-
.../GpuGraphicsState.cs | 10 +++++++++-
src/Ryujinx.Graphics.Shader/IGpuAccessor.cs | 1 +
.../Instructions/InstEmitAttribute.cs | 10 ++++++++++
.../Optimizations/BindlessElimination.cs | 4 ++--
.../Translation/ShaderDefinitions.cs | 2 ++
src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs | 3 ++-
14 files changed, 76 insertions(+), 17 deletions(-)
diff --git a/src/Ryujinx.Graphics.GAL/Capabilities.cs b/src/Ryujinx.Graphics.GAL/Capabilities.cs
index 70736fbd6..779ce5b5d 100644
--- a/src/Ryujinx.Graphics.GAL/Capabilities.cs
+++ b/src/Ryujinx.Graphics.GAL/Capabilities.cs
@@ -36,6 +36,7 @@ namespace Ryujinx.Graphics.GAL
public readonly bool SupportsMismatchingViewFormat;
public readonly bool SupportsCubemapView;
public readonly bool SupportsNonConstantTextureOffset;
+ public readonly bool SupportsQuads;
public readonly bool SupportsSeparateSampler;
public readonly bool SupportsShaderBallot;
public readonly bool SupportsShaderBarrierDivergence;
@@ -93,6 +94,7 @@ namespace Ryujinx.Graphics.GAL
bool supportsMismatchingViewFormat,
bool supportsCubemapView,
bool supportsNonConstantTextureOffset,
+ bool supportsQuads,
bool supportsSeparateSampler,
bool supportsShaderBallot,
bool supportsShaderBarrierDivergence,
@@ -146,6 +148,7 @@ namespace Ryujinx.Graphics.GAL
SupportsMismatchingViewFormat = supportsMismatchingViewFormat;
SupportsCubemapView = supportsCubemapView;
SupportsNonConstantTextureOffset = supportsNonConstantTextureOffset;
+ SupportsQuads = supportsQuads;
SupportsSeparateSampler = supportsSeparateSampler;
SupportsShaderBallot = supportsShaderBallot;
SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence;
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs
index 45f32e2d3..3c7664b77 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGpuAccessor.cs
@@ -18,6 +18,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
private readonly ShaderSpecializationState _newSpecState;
private readonly int _stageIndex;
private readonly bool _isVulkan;
+ private readonly bool _hasGeometryShader;
+ private readonly bool _supportsQuads;
///
/// Creates a new instance of the cached GPU state accessor for shader translation.
@@ -29,6 +31,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
/// Shader specialization state of the recompiled shader
/// Resource counts shared across all shader stages
/// Shader stage index
+ /// Indicates if a geometry shader is present
public DiskCacheGpuAccessor(
GpuContext context,
ReadOnlyMemory data,
@@ -36,7 +39,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
ShaderSpecializationState oldSpecState,
ShaderSpecializationState newSpecState,
ResourceCounts counts,
- int stageIndex) : base(context, counts, stageIndex)
+ int stageIndex,
+ bool hasGeometryShader) : base(context, counts, stageIndex)
{
_data = data;
_cb1Data = cb1Data;
@@ -44,6 +48,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
_newSpecState = newSpecState;
_stageIndex = stageIndex;
_isVulkan = context.Capabilities.Api == TargetApi.Vulkan;
+ _hasGeometryShader = hasGeometryShader;
+ _supportsQuads = context.Capabilities.SupportsQuads;
if (stageIndex == (int)ShaderStage.Geometry - 1)
{
@@ -100,7 +106,11 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
///
public GpuGraphicsState QueryGraphicsState()
{
- return _oldSpecState.GraphicsState.CreateShaderGraphicsState(!_isVulkan, _isVulkan || _oldSpecState.GraphicsState.YNegateEnabled);
+ return _oldSpecState.GraphicsState.CreateShaderGraphicsState(
+ !_isVulkan,
+ _supportsQuads,
+ _hasGeometryShader,
+ _isVulkan || _oldSpecState.GraphicsState.YNegateEnabled);
}
///
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
index 2c19cc4b9..ea54049c2 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
@@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
private const ushort FileFormatVersionMajor = 1;
private const ushort FileFormatVersionMinor = 2;
private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
- private const uint CodeGenVersion = 6577;
+ private const uint CodeGenVersion = 5936;
private const string SharedTocFileName = "shared.toc";
private const string SharedDataFileName = "shared.data";
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs
index 153fc4427..20f96462e 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs
@@ -601,6 +601,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
TargetApi api = _context.Capabilities.Api;
+ bool hasCachedGs = guestShaders[4].HasValue;
+
for (int stageIndex = Constants.ShaderStages - 1; stageIndex >= 0; stageIndex--)
{
if (guestShaders[stageIndex + 1].HasValue)
@@ -610,7 +612,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
byte[] guestCode = shader.Code;
byte[] cb1Data = shader.Cb1Data;
- DiskCacheGpuAccessor gpuAccessor = new(_context, guestCode, cb1Data, specState, newSpecState, counts, stageIndex);
+ DiskCacheGpuAccessor gpuAccessor = new(_context, guestCode, cb1Data, specState, newSpecState, counts, stageIndex, hasCachedGs);
TranslatorContext currentStage = DecodeGraphicsShader(gpuAccessor, api, DefaultFlags, 0);
if (nextStage != null)
@@ -623,7 +625,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
byte[] guestCodeA = guestShaders[0].Value.Code;
byte[] cb1DataA = guestShaders[0].Value.Cb1Data;
- DiskCacheGpuAccessor gpuAccessorA = new(_context, guestCodeA, cb1DataA, specState, newSpecState, counts, 0);
+ DiskCacheGpuAccessor gpuAccessorA = new(_context, guestCodeA, cb1DataA, specState, newSpecState, counts, 0, hasCachedGs);
translatorContexts[0] = DecodeGraphicsShader(gpuAccessorA, api, DefaultFlags | TranslationFlags.VertexA, 0);
}
@@ -711,7 +713,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
GuestCodeAndCbData shader = guestShaders[0].Value;
ResourceCounts counts = new();
ShaderSpecializationState newSpecState = new(ref specState.ComputeState);
- DiskCacheGpuAccessor gpuAccessor = new(_context, shader.Code, shader.Cb1Data, specState, newSpecState, counts, 0);
+ DiskCacheGpuAccessor gpuAccessor = new(_context, shader.Code, shader.Cb1Data, specState, newSpecState, counts, 0, false);
gpuAccessor.InitializeReservedCounts(tfEnabled: false, vertexAsCompute: false);
TranslatorContext translatorContext = DecodeComputeShader(gpuAccessor, _context.Capabilities.Api, 0);
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs
index 04949690a..1be75f242 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs
@@ -17,6 +17,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
private readonly int _stageIndex;
private readonly bool _compute;
private readonly bool _isVulkan;
+ private readonly bool _hasGeometryShader;
+ private readonly bool _supportsQuads;
///
/// Creates a new instance of the GPU state accessor for graphics shader translation.
@@ -25,12 +27,20 @@ namespace Ryujinx.Graphics.Gpu.Shader
/// GPU channel
/// Current GPU state
/// Graphics shader stage index (0 = Vertex, 4 = Fragment)
- public GpuAccessor(GpuContext context, GpuChannel channel, GpuAccessorState state, int stageIndex) : base(context, state.ResourceCounts, stageIndex)
+ /// Indicates if a geometry shader is present
+ public GpuAccessor(
+ GpuContext context,
+ GpuChannel channel,
+ GpuAccessorState state,
+ int stageIndex,
+ bool hasGeometryShader) : base(context, state.ResourceCounts, stageIndex)
{
- _isVulkan = context.Capabilities.Api == TargetApi.Vulkan;
_channel = channel;
_state = state;
_stageIndex = stageIndex;
+ _isVulkan = context.Capabilities.Api == TargetApi.Vulkan;
+ _hasGeometryShader = hasGeometryShader;
+ _supportsQuads = context.Capabilities.SupportsQuads;
if (stageIndex == (int)ShaderStage.Geometry - 1)
{
@@ -105,7 +115,11 @@ namespace Ryujinx.Graphics.Gpu.Shader
///
public GpuGraphicsState QueryGraphicsState()
{
- return _state.GraphicsState.CreateShaderGraphicsState(!_isVulkan, _isVulkan || _state.GraphicsState.YNegateEnabled);
+ return _state.GraphicsState.CreateShaderGraphicsState(
+ !_isVulkan,
+ _supportsQuads,
+ _hasGeometryShader,
+ _isVulkan || _state.GraphicsState.YNegateEnabled);
}
///
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelGraphicsState.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelGraphicsState.cs
index b5bc4df3c..765bef7d4 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelGraphicsState.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelGraphicsState.cs
@@ -106,8 +106,11 @@ namespace Ryujinx.Graphics.Gpu.Shader
/// Creates a new graphics state from this state that can be used for shader generation.
///
/// Indicates if the host API supports alpha test operations
+ /// Indicates if the host API supports quad primitives
+ /// Indicates if a geometry shader is used
+ /// If true, indicates that the fragment origin is the upper left corner of the viewport, otherwise it is the lower left corner
/// GPU graphics state that can be used for shader translation
- public readonly GpuGraphicsState CreateShaderGraphicsState(bool hostSupportsAlphaTest, bool originUpperLeft)
+ public readonly GpuGraphicsState CreateShaderGraphicsState(bool hostSupportsAlphaTest, bool hostSupportsQuads, bool hasGeometryShader, bool originUpperLeft)
{
AlphaTestOp alphaTestOp;
@@ -130,6 +133,9 @@ namespace Ryujinx.Graphics.Gpu.Shader
};
}
+ bool isQuad = Topology == PrimitiveTopology.Quads || Topology == PrimitiveTopology.QuadStrip;
+ bool halvePrimitiveId = !hostSupportsQuads && !hasGeometryShader && isQuad;
+
return new GpuGraphicsState(
EarlyZForce,
ConvertToInputTopology(Topology, TessellationMode),
@@ -149,7 +155,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
in FragmentOutputTypes,
DualSourceBlendEnable,
YNegateEnabled,
- originUpperLeft);
+ originUpperLeft,
+ halvePrimitiveId);
}
///
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs
index 31cc94a25..4fc66c4c0 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs
@@ -339,7 +339,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
if (gpuVa != 0)
{
- GpuAccessor gpuAccessor = new(_context, channel, gpuAccessorState, stageIndex);
+ GpuAccessor gpuAccessor = new(_context, channel, gpuAccessorState, stageIndex, addresses.Geometry != 0);
TranslatorContext currentStage = DecodeGraphicsShader(gpuAccessor, api, DefaultFlags, gpuVa);
if (nextStage != null)
diff --git a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
index d56c40af4..2a39ae446 100644
--- a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
+++ b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
@@ -161,6 +161,7 @@ namespace Ryujinx.Graphics.OpenGL
supportsBgraFormat: false,
supportsR4G4Format: false,
supportsR4G4B4A4Format: true,
+ supportsScaledVertexFormats: true,
supportsSnormBufferTextureFormat: false,
supports5BitComponentFormat: true,
supportsSparseBuffer: false,
@@ -175,7 +176,7 @@ namespace Ryujinx.Graphics.OpenGL
supportsMismatchingViewFormat: HwCapabilities.SupportsMismatchingViewFormat,
supportsCubemapView: true,
supportsNonConstantTextureOffset: HwCapabilities.SupportsNonConstantTextureOffset,
- supportsScaledVertexFormats: true,
+ supportsQuads: HwCapabilities.SupportsQuads,
supportsSeparateSampler: false,
supportsShaderBallot: HwCapabilities.SupportsShaderBallot,
supportsShaderBarrierDivergence: !(intelWindows || intelUnix),
diff --git a/src/Ryujinx.Graphics.Shader/GpuGraphicsState.cs b/src/Ryujinx.Graphics.Shader/GpuGraphicsState.cs
index f16c71d55..38684002c 100644
--- a/src/Ryujinx.Graphics.Shader/GpuGraphicsState.cs
+++ b/src/Ryujinx.Graphics.Shader/GpuGraphicsState.cs
@@ -102,6 +102,11 @@ namespace Ryujinx.Graphics.Shader
///
public readonly bool OriginUpperLeft;
+ ///
+ /// Indicates that the primitive ID values on the shader should be halved due to quad to triangles conversion.
+ ///
+ public readonly bool HalvePrimitiveId;
+
///
/// Creates a new GPU graphics state.
///
@@ -124,6 +129,7 @@ namespace Ryujinx.Graphics.Shader
/// Indicates whether dual source blend is enabled
/// Indicates if negation of the viewport Y axis is enabled
/// If true, indicates that the fragment origin is the upper left corner of the viewport, otherwise it is the lower left corner
+ /// Indicates that the primitive ID values on the shader should be halved due to quad to triangles conversion
public GpuGraphicsState(
bool earlyZForce,
InputTopology topology,
@@ -143,7 +149,8 @@ namespace Ryujinx.Graphics.Shader
in Array8 fragmentOutputTypes,
bool dualSourceBlendEnable,
bool yNegateEnabled,
- bool originUpperLeft)
+ bool originUpperLeft,
+ bool halvePrimitiveId)
{
EarlyZForce = earlyZForce;
Topology = topology;
@@ -164,6 +171,7 @@ namespace Ryujinx.Graphics.Shader
DualSourceBlendEnable = dualSourceBlendEnable;
YNegateEnabled = yNegateEnabled;
OriginUpperLeft = originUpperLeft;
+ HalvePrimitiveId = halvePrimitiveId;
}
}
}
diff --git a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs
index b1a9f9f84..3dc4ad907 100644
--- a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs
+++ b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs
@@ -135,6 +135,7 @@ namespace Ryujinx.Graphics.Shader
default,
false,
false,
+ false,
false);
}
diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs
index 63ce38e25..c704156bc 100644
--- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs
+++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitAttribute.cs
@@ -84,6 +84,10 @@ namespace Ryujinx.Graphics.Shader.Instructions
value = context.IConvertU32ToFP32(value);
}
}
+ else if (offset == AttributeConsts.PrimitiveId && context.TranslatorContext.Definitions.HalvePrimitiveId)
+ {
+ value = context.ShiftRightS32(value, Const(1));
+ }
context.Copy(Register(rd), value);
}
@@ -187,6 +191,12 @@ namespace Ryujinx.Graphics.Shader.Instructions
}
}
}
+ else if (op.Imm10 == AttributeConsts.PrimitiveId && context.TranslatorContext.Definitions.HalvePrimitiveId)
+ {
+ // If quads are used, but the host does not support them, they need to be converted to triangles.
+ // Since each quad becomes 2 triangles, we need to compensate here and divide primitive ID by 2.
+ res = context.ShiftRightS32(res, Const(1));
+ }
else if (op.Imm10 == AttributeConsts.FrontFacing && context.TranslatorContext.GpuAccessor.QueryHostHasFrontFacingBug())
{
// gl_FrontFacing sometimes has incorrect (flipped) values depending how it is accessed on Intel GPUs.
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
index 223215439..4128af241 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
@@ -66,9 +66,9 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
if (nvHandle.AsgOp is not Operation handleOp ||
handleOp.Inst != Instruction.Load ||
- handleOp.StorageKind != StorageKind.Input)
+ (handleOp.StorageKind != StorageKind.Input && handleOp.StorageKind != StorageKind.StorageBuffer))
{
- // Right now, we only allow bindless access when the handle comes from a shader input.
+ // Right now, we only allow bindless access when the handle comes from a shader input or storage buffer.
// This is an artificial limitation to prevent it from being used in cases where it
// would have a large performance impact of loading all textures in the pool.
// It might be removed in the future, if we can mitigate the performance impact.
diff --git a/src/Ryujinx.Graphics.Shader/Translation/ShaderDefinitions.cs b/src/Ryujinx.Graphics.Shader/Translation/ShaderDefinitions.cs
index 3246e2594..f831ec940 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/ShaderDefinitions.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/ShaderDefinitions.cs
@@ -45,6 +45,8 @@ namespace Ryujinx.Graphics.Shader.Translation
public bool YNegateEnabled => _graphicsState.YNegateEnabled;
public bool OriginUpperLeft => _graphicsState.OriginUpperLeft;
+ public bool HalvePrimitiveId => _graphicsState.HalvePrimitiveId;
+
public ImapPixelType[] ImapTypes { get; }
public bool IaIndexing { get; private set; }
public bool OaIndexing { get; private set; }
diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
index b46ba9c46..8ef05de36 100644
--- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
+++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
@@ -691,6 +691,7 @@ namespace Ryujinx.Graphics.Vulkan
supportsBgraFormat: true,
supportsR4G4Format: false,
supportsR4G4B4A4Format: supportsR4G4B4A4Format,
+ supportsScaledVertexFormats: FormatCapabilities.SupportsScaledVertexFormats(),
supportsSnormBufferTextureFormat: true,
supports5BitComponentFormat: supports5BitComponentFormat,
supportsSparseBuffer: features2.Features.SparseBinding && mainQueueProperties.QueueFlags.HasFlag(QueueFlags.SparseBindingBit),
@@ -705,7 +706,7 @@ namespace Ryujinx.Graphics.Vulkan
supportsMismatchingViewFormat: true,
supportsCubemapView: !IsAmdGcn,
supportsNonConstantTextureOffset: false,
- supportsScaledVertexFormats: FormatCapabilities.SupportsScaledVertexFormats(),
+ supportsQuads: false,
supportsSeparateSampler: true,
supportsShaderBallot: false,
supportsShaderBarrierDivergence: Vendor != Vendor.Intel,
From 075575200d693a5d24bd03a4220238e1e50f1ef6 Mon Sep 17 00:00:00 2001
From: Gavin Zyonse
Date: Tue, 14 May 2024 16:58:48 +0200
Subject: [PATCH 010/109] Update compatibility information in README.md (#6801)
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index f2f3cb001..7f2294d31 100644
--- a/README.md
+++ b/README.md
@@ -36,8 +36,8 @@
## Compatibility
-As of October 2023, Ryujinx has been tested on approximately 4,200 titles;
-over 4,150 boot past menus and into gameplay, with roughly 3,500 of those being considered playable.
+As of May 2024, Ryujinx has been tested on approximately 4,300 titles;
+over 4,100 boot past menus and into gameplay, with roughly 3,550 of those being considered playable.
You can check out the compatibility list [here](https://github.com/Ryujinx/Ryujinx-Games-List/issues).
From 2b6cc4b3536693d222b695295c9db4715ca3f570 Mon Sep 17 00:00:00 2001
From: yell0wsuit <5692900+yell0wsuit@users.noreply.github.com>
Date: Tue, 14 May 2024 22:00:03 +0700
Subject: [PATCH 011/109] Add the "Auto" theme option in setting (#6611)
* Add "Follow OS theme" option
* Update App.axaml.cs
* Add "Follow OS theme" option
* Update App.axaml.cs
* Remove `this`
* Remove annotation for nullable reference
* Change into switch expression to make it concise
* Change comments to XML docs
* Update en_US.json
* Fix icons in About dialog do not response to "auto" theme
The theme icons seemingly use Dark variant event when the OS theme is light. In addition, I added ThemeManager common to make it accessible for both App and AboutWindow
* Newline at the end
* newline moment
* Update ThemeManager.cs
* bait to switch to lf
* change to lf
* temp. revert
* Add back ThemeManager.cs common, pls pass the format check
* I found the mistake: should have put `ThemeManager.OnThemeChanged();` in try block
Finally solve the formatting check
* test formatting
* Update App.axaml.cs
* Ok i seem to forget to add version lol
* Fix info CA1816
---
src/Ryujinx/App.axaml.cs | 34 +++++++++++-
src/Ryujinx/Assets/Locales/en_US.json | 1 +
src/Ryujinx/Common/ThemeManager.cs | 14 +++++
.../UI/ViewModels/AboutWindowViewModel.cs | 52 +++++++++++++------
.../UI/ViewModels/SettingsViewModel.cs | 16 +++++-
.../UI/Views/Settings/SettingsUIView.axaml | 3 ++
src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 29 +++++++++++
7 files changed, 128 insertions(+), 21 deletions(-)
create mode 100644 src/Ryujinx/Common/ThemeManager.cs
diff --git a/src/Ryujinx/App.axaml.cs b/src/Ryujinx/App.axaml.cs
index 387a6dc14..24d8a70a1 100644
--- a/src/Ryujinx/App.axaml.cs
+++ b/src/Ryujinx/App.axaml.cs
@@ -1,8 +1,10 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
+using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.Threading;
+using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Windows;
@@ -84,7 +86,7 @@ namespace Ryujinx.Ava
ApplyConfiguredTheme();
}
- private void ApplyConfiguredTheme()
+ public void ApplyConfiguredTheme()
{
try
{
@@ -92,13 +94,18 @@ namespace Ryujinx.Ava
if (string.IsNullOrWhiteSpace(baseStyle))
{
- ConfigurationState.Instance.UI.BaseStyle.Value = "Dark";
+ ConfigurationState.Instance.UI.BaseStyle.Value = "Auto";
baseStyle = ConfigurationState.Instance.UI.BaseStyle;
}
+ ThemeVariant systemTheme = DetectSystemTheme();
+
+ ThemeManager.OnThemeChanged();
+
RequestedThemeVariant = baseStyle switch
{
+ "Auto" => systemTheme,
"Light" => ThemeVariant.Light,
"Dark" => ThemeVariant.Dark,
_ => ThemeVariant.Default,
@@ -111,5 +118,28 @@ namespace Ryujinx.Ava
ShowRestartDialog();
}
}
+
+ ///
+ /// Converts a PlatformThemeVariant value to the corresponding ThemeVariant value.
+ ///
+ public static ThemeVariant ConvertThemeVariant(PlatformThemeVariant platformThemeVariant) =>
+ platformThemeVariant switch
+ {
+ PlatformThemeVariant.Dark => ThemeVariant.Dark,
+ PlatformThemeVariant.Light => ThemeVariant.Light,
+ _ => ThemeVariant.Default,
+ };
+
+ public static ThemeVariant DetectSystemTheme()
+ {
+ if (Application.Current is App app)
+ {
+ var colorValues = app.PlatformSettings.GetColorValues();
+
+ return ConvertThemeVariant(colorValues.ThemeVariant);
+ }
+
+ return ThemeVariant.Default;
+ }
}
}
diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json
index 2299d9e66..8df0f96a1 100644
--- a/src/Ryujinx/Assets/Locales/en_US.json
+++ b/src/Ryujinx/Assets/Locales/en_US.json
@@ -404,6 +404,7 @@
"GameListContextMenuToggleFavorite": "Toggle Favorite",
"GameListContextMenuToggleFavoriteToolTip": "Toggle Favorite status of Game",
"SettingsTabGeneralTheme": "Theme:",
+ "SettingsTabGeneralThemeAuto": "Auto",
"SettingsTabGeneralThemeDark": "Dark",
"SettingsTabGeneralThemeLight": "Light",
"ControllerSettingsConfigureGeneral": "Configure",
diff --git a/src/Ryujinx/Common/ThemeManager.cs b/src/Ryujinx/Common/ThemeManager.cs
new file mode 100644
index 000000000..8c52c2a66
--- /dev/null
+++ b/src/Ryujinx/Common/ThemeManager.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Ryujinx.Ava.Common
+{
+ public static class ThemeManager
+ {
+ public static event EventHandler ThemeChanged;
+
+ public static void OnThemeChanged()
+ {
+ ThemeChanged?.Invoke(null, EventArgs.Empty);
+ }
+ }
+}
diff --git a/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs
index 6020f40e0..f8fd5b7de 100644
--- a/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs
@@ -1,6 +1,8 @@
using Avalonia.Media.Imaging;
using Avalonia.Platform;
+using Avalonia.Styling;
using Avalonia.Threading;
+using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Common.Utilities;
using Ryujinx.UI.Common.Configuration;
@@ -11,7 +13,7 @@ using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.ViewModels
{
- public class AboutWindowViewModel : BaseModel
+ public class AboutWindowViewModel : BaseModel, IDisposable
{
private Bitmap _githubLogo;
private Bitmap _discordLogo;
@@ -86,23 +88,39 @@ namespace Ryujinx.Ava.UI.ViewModels
public AboutWindowViewModel()
{
Version = Program.Version;
-
- if (ConfigurationState.Instance.UI.BaseStyle.Value == "Light")
- {
- GithubLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_GitHub_Light.png?assembly=Ryujinx.UI.Common")));
- DiscordLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_Discord_Light.png?assembly=Ryujinx.UI.Common")));
- PatreonLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_Patreon_Light.png?assembly=Ryujinx.UI.Common")));
- TwitterLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_Twitter_Light.png?assembly=Ryujinx.UI.Common")));
- }
- else
- {
- GithubLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_GitHub_Dark.png?assembly=Ryujinx.UI.Common")));
- DiscordLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_Discord_Dark.png?assembly=Ryujinx.UI.Common")));
- PatreonLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_Patreon_Dark.png?assembly=Ryujinx.UI.Common")));
- TwitterLogo = new Bitmap(AssetLoader.Open(new Uri("resm:Ryujinx.UI.Common.Resources.Logo_Twitter_Dark.png?assembly=Ryujinx.UI.Common")));
- }
-
+ UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value);
Dispatcher.UIThread.InvokeAsync(DownloadPatronsJson);
+
+ ThemeManager.ThemeChanged += ThemeManager_ThemeChanged;
+ }
+
+ private void ThemeManager_ThemeChanged(object sender, EventArgs e)
+ {
+ Dispatcher.UIThread.Post(() => UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value));
+ }
+
+ private void UpdateLogoTheme(string theme)
+ {
+ bool isDarkTheme = theme == "Dark" || (theme == "Auto" && App.DetectSystemTheme() == ThemeVariant.Dark);
+
+ string basePath = "resm:Ryujinx.UI.Common.Resources.";
+ string themeSuffix = isDarkTheme ? "Dark.png" : "Light.png";
+
+ GithubLogo = LoadBitmap($"{basePath}Logo_GitHub_{themeSuffix}?assembly=Ryujinx.UI.Common");
+ DiscordLogo = LoadBitmap($"{basePath}Logo_Discord_{themeSuffix}?assembly=Ryujinx.UI.Common");
+ PatreonLogo = LoadBitmap($"{basePath}Logo_Patreon_{themeSuffix}?assembly=Ryujinx.UI.Common");
+ TwitterLogo = LoadBitmap($"{basePath}Logo_Twitter_{themeSuffix}?assembly=Ryujinx.UI.Common");
+ }
+
+ private Bitmap LoadBitmap(string uri)
+ {
+ return new Bitmap(Avalonia.Platform.AssetLoader.Open(new Uri(uri)));
+ }
+
+ public void Dispose()
+ {
+ ThemeManager.ThemeChanged -= ThemeManager_ThemeChanged;
+ GC.SuppressFinalize(this);
}
private async Task DownloadPatronsJson()
diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
index 0f43d0f7f..70e5fa5c7 100644
--- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs
@@ -397,7 +397,13 @@ namespace Ryujinx.Ava.UI.ViewModels
GameDirectories.Clear();
GameDirectories.AddRange(config.UI.GameDirs.Value);
- BaseStyleIndex = config.UI.BaseStyle == "Light" ? 0 : 1;
+ BaseStyleIndex = config.UI.BaseStyle.Value switch
+ {
+ "Auto" => 0,
+ "Light" => 1,
+ "Dark" => 2,
+ _ => 0
+ };
// Input
EnableDockedMode = config.System.EnableDockedMode;
@@ -486,7 +492,13 @@ namespace Ryujinx.Ava.UI.ViewModels
config.UI.GameDirs.Value = gameDirs;
}
- config.UI.BaseStyle.Value = BaseStyleIndex == 0 ? "Light" : "Dark";
+ config.UI.BaseStyle.Value = BaseStyleIndex switch
+ {
+ 0 => "Auto",
+ 1 => "Light",
+ 2 => "Dark",
+ _ => "Auto"
+ };
// Input
config.System.EnableDockedMode.Value = EnableDockedMode;
diff --git a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml
index b60058fcb..f9b9be44b 100644
--- a/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml
+++ b/src/Ryujinx/UI/Views/Settings/SettingsUIView.axaml
@@ -65,6 +65,9 @@
+
+
+
diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs
index b1b7a4853..7de8a49a0 100644
--- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs
+++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs
@@ -2,6 +2,7 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
+using Avalonia.Platform;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common;
@@ -92,6 +93,29 @@ namespace Ryujinx.Ava.UI.Windows
}
}
+ ///
+ /// Event handler for detecting OS theme change when using "Follow OS theme" option
+ ///
+ private void OnPlatformColorValuesChanged(object sender, PlatformColorValues e)
+ {
+ if (Application.Current is App app)
+ {
+ app.ApplyConfiguredTheme();
+ }
+ }
+
+ protected override void OnClosed(EventArgs e)
+ {
+ base.OnClosed(e);
+ if (PlatformSettings != null)
+ {
+ ///
+ /// Unsubscribe to the ColorValuesChanged event
+ ///
+ PlatformSettings.ColorValuesChanged -= OnPlatformColorValuesChanged;
+ }
+ }
+
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
@@ -390,6 +414,11 @@ namespace Ryujinx.Ava.UI.Windows
Initialize();
+ ///
+ /// Subscribe to the ColorValuesChanged event
+ ///
+ PlatformSettings.ColorValuesChanged += OnPlatformColorValuesChanged;
+
ViewModel.Initialize(
ContentManager,
StorageProvider,
From 2ef4f92b0793feb7073ed85b7f7dc08dca6f14e9 Mon Sep 17 00:00:00 2001
From: gdkchan
Date: Tue, 14 May 2024 12:06:36 -0300
Subject: [PATCH 012/109] Make TextureGroup.ClearModified thread safe (#6686)
---
src/Ryujinx.Graphics.Gpu/Image/Texture.cs | 2 +-
src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs | 18 ++++++++++++------
.../Image/TextureGroupHandle.cs | 6 +++---
3 files changed, 16 insertions(+), 10 deletions(-)
diff --git a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs
index e67caea81..dde28dbd7 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/Texture.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/Texture.cs
@@ -390,7 +390,7 @@ namespace Ryujinx.Graphics.Gpu.Image
{
_views.Remove(texture);
- Group.RemoveView(texture);
+ Group.RemoveView(_views, texture);
texture._viewStorage = texture;
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
index de9c47c97..4e1133d1a 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
@@ -88,9 +88,9 @@ namespace Ryujinx.Graphics.Gpu.Image
private MultiRange TextureRange => Storage.Range;
///
- /// The views list from the storage texture.
+ /// The views array from the storage texture.
///
- private List _views;
+ private Texture[] _views;
private TextureGroupHandle[] _handles;
private bool[] _loadNeeded;
@@ -1074,7 +1074,7 @@ namespace Ryujinx.Graphics.Gpu.Image
public void UpdateViews(List views, Texture texture)
{
// This is saved to calculate overlapping views for each handle.
- _views = views;
+ _views = views.ToArray();
bool layerViews = _hasLayerViews;
bool mipViews = _hasMipViews;
@@ -1136,9 +1136,13 @@ namespace Ryujinx.Graphics.Gpu.Image
///
/// Removes a view from the group, removing it from all overlap lists.
///
+ /// The views list of the storage texture
/// View to remove from the group
- public void RemoveView(Texture view)
+ public void RemoveView(List views, Texture view)
{
+ // This is saved to calculate overlapping views for each handle.
+ _views = views.ToArray();
+
int offset = FindOffset(view);
foreach (TextureGroupHandle handle in _handles)
@@ -1605,9 +1609,11 @@ namespace Ryujinx.Graphics.Gpu.Image
Storage.SignalModifiedDirty();
- if (_views != null)
+ Texture[] views = _views;
+
+ if (views != null)
{
- foreach (Texture texture in _views)
+ foreach (Texture texture in views)
{
texture.SignalModifiedDirty();
}
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs
index 0af6b7ca8..860922d59 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroupHandle.cs
@@ -121,7 +121,7 @@ namespace Ryujinx.Graphics.Gpu.Image
public TextureGroupHandle(TextureGroup group,
int offset,
ulong size,
- List views,
+ IEnumerable views,
int firstLayer,
int firstLevel,
int baseSlice,
@@ -201,8 +201,8 @@ namespace Ryujinx.Graphics.Gpu.Image
/// Calculate a list of which views overlap this handle.
///
/// The parent texture group, used to find a view's base CPU VA offset
- /// The list of views to search for overlaps
- public void RecalculateOverlaps(TextureGroup group, List views)
+ /// The views to search for overlaps
+ public void RecalculateOverlaps(TextureGroup group, IEnumerable views)
{
// Overlaps can be accessed from the memory tracking signal handler, so access must be atomic.
lock (Overlaps)
From a3dc295c5f867bddb56a38f3a848ceb61ff30d32 Mon Sep 17 00:00:00 2001
From: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
Date: Tue, 14 May 2024 17:14:39 +0200
Subject: [PATCH 013/109] Disable keyboard controller input while swkbd is open
(foreground) (#6646)
* Block input updates while swkbd is open in foreground mode
* Flush internal driver state before unblocking input updates
* Rename Flush to Clear and remove unnecessary attribute
---
src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs | 5 +++++
src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs | 3 +++
src/Ryujinx.Input/HLE/NpadManager.cs | 5 +++++
src/Ryujinx.Input/IGamepadDriver.cs | 6 ++++++
src/Ryujinx/Input/AvaloniaKeyboard.cs | 2 +-
src/Ryujinx/Input/AvaloniaKeyboardDriver.cs | 2 +-
src/Ryujinx/UI/Applet/AvaHostUIHandler.cs | 2 ++
7 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs b/src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs
index e502254be..bd71c7933 100644
--- a/src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs
+++ b/src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs
@@ -81,6 +81,11 @@ namespace Ryujinx.Input.GTK3
return _pressedKeys.Contains(nativeKey);
}
+ public void Clear()
+ {
+ _pressedKeys.Clear();
+ }
+
public IGamepad GetGamepad(string id)
{
if (!_keyboardIdentifers[0].Equals(id))
diff --git a/src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs b/src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs
index 1d918d21b..b3f509a09 100644
--- a/src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs
+++ b/src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs
@@ -107,6 +107,8 @@ namespace Ryujinx.UI.Applet
swkbdDialog.SetInputLengthValidation(args.StringLengthMin, args.StringLengthMax);
swkbdDialog.SetInputValidation(args.KeyboardMode);
+ ((MainWindow)_parent).RendererWidget.NpadManager.BlockInputUpdates();
+
if (swkbdDialog.Run() == (int)ResponseType.Ok)
{
inputText = swkbdDialog.InputEntry.Text;
@@ -128,6 +130,7 @@ namespace Ryujinx.UI.Applet
});
dialogCloseEvent.WaitOne();
+ ((MainWindow)_parent).RendererWidget.NpadManager.UnblockInputUpdates();
userText = error ? null : inputText;
diff --git a/src/Ryujinx.Input/HLE/NpadManager.cs b/src/Ryujinx.Input/HLE/NpadManager.cs
index 4c7bb8b7a..2409ecf22 100644
--- a/src/Ryujinx.Input/HLE/NpadManager.cs
+++ b/src/Ryujinx.Input/HLE/NpadManager.cs
@@ -174,6 +174,11 @@ namespace Ryujinx.Input.HLE
{
lock (_lock)
{
+ foreach (InputConfig inputConfig in _inputConfig)
+ {
+ _controllers[(int)inputConfig.PlayerIndex].GamepadDriver.Clear();
+ }
+
_blockInputUpdates = false;
}
}
diff --git a/src/Ryujinx.Input/IGamepadDriver.cs b/src/Ryujinx.Input/IGamepadDriver.cs
index 67b01c26c..ff4d36993 100644
--- a/src/Ryujinx.Input/IGamepadDriver.cs
+++ b/src/Ryujinx.Input/IGamepadDriver.cs
@@ -33,5 +33,11 @@ namespace Ryujinx.Input
/// The unique id of the gamepad
/// An instance of associated to the gamepad id given or null if not found
IGamepad GetGamepad(string id);
+
+ ///
+ /// Flush the internal state of the driver.
+ ///
+ /// Does nothing by default.
+ void Clear() { }
}
}
diff --git a/src/Ryujinx/Input/AvaloniaKeyboard.cs b/src/Ryujinx/Input/AvaloniaKeyboard.cs
index fbaaaabab..ff88de79e 100644
--- a/src/Ryujinx/Input/AvaloniaKeyboard.cs
+++ b/src/Ryujinx/Input/AvaloniaKeyboard.cs
@@ -195,7 +195,7 @@ namespace Ryujinx.Ava.Input
public void Clear()
{
- _driver?.ResetKeys();
+ _driver?.Clear();
}
public void Dispose() { }
diff --git a/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs b/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs
index e9e71b99b..9f87e821a 100644
--- a/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs
+++ b/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs
@@ -94,7 +94,7 @@ namespace Ryujinx.Ava.Input
return _pressedKeys.Contains(nativeKey);
}
- public void ResetKeys()
+ public void Clear()
{
_pressedKeys.Clear();
}
diff --git a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
index 4bcc35a7a..4bcf8eb94 100644
--- a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
+++ b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
@@ -122,6 +122,7 @@ namespace Ryujinx.Ava.UI.Applet
{
try
{
+ _parent.ViewModel.AppHost.NpadManager.BlockInputUpdates();
var response = await SwkbdAppletDialog.ShowInputDialog(LocaleManager.Instance[LocaleKeys.SoftwareKeyboard], args);
if (response.Result == UserResult.Ok)
@@ -143,6 +144,7 @@ namespace Ryujinx.Ava.UI.Applet
});
dialogCloseEvent.WaitOne();
+ _parent.ViewModel.AppHost.NpadManager.UnblockInputUpdates();
userText = error ? null : inputText;
From cd78adf07f14ccb8b19a9f539db963dea5976312 Mon Sep 17 00:00:00 2001
From: gdkchan
Date: Tue, 14 May 2024 12:23:13 -0300
Subject: [PATCH 014/109] Add missing lock on texture cache UpdateMapping
method (#6657)
---
.../Image/TextureCache.cs | 95 ++++++++++++++++---
1 file changed, 84 insertions(+), 11 deletions(-)
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs
index 5743c89c0..b9ff060e2 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureCache.cs
@@ -8,6 +8,7 @@ using Ryujinx.Graphics.Texture;
using Ryujinx.Memory.Range;
using System;
using System.Collections.Generic;
+using System.Threading;
namespace Ryujinx.Graphics.Gpu.Image
{
@@ -39,6 +40,8 @@ namespace Ryujinx.Graphics.Gpu.Image
private readonly MultiRangeList _textures;
private readonly HashSet _partiallyMappedTextures;
+ private readonly ReaderWriterLockSlim _texturesLock;
+
private Texture[] _textureOverlaps;
private OverlapInfo[] _overlapInfo;
@@ -57,6 +60,8 @@ namespace Ryujinx.Graphics.Gpu.Image
_textures = new MultiRangeList();
_partiallyMappedTextures = new HashSet();
+ _texturesLock = new ReaderWriterLockSlim();
+
_textureOverlaps = new Texture[OverlapsBufferInitialCapacity];
_overlapInfo = new OverlapInfo[OverlapsBufferInitialCapacity];
@@ -75,10 +80,16 @@ namespace Ryujinx.Graphics.Gpu.Image
MultiRange unmapped = ((MemoryManager)sender).GetPhysicalRegions(e.Address, e.Size);
- lock (_textures)
+ _texturesLock.EnterReadLock();
+
+ try
{
overlapCount = _textures.FindOverlaps(unmapped, ref overlaps);
}
+ finally
+ {
+ _texturesLock.ExitReadLock();
+ }
if (overlapCount > 0)
{
@@ -217,7 +228,18 @@ namespace Ryujinx.Graphics.Gpu.Image
public bool UpdateMapping(Texture texture, MultiRange range)
{
// There cannot be an existing texture compatible with this mapping in the texture cache already.
- int overlapCount = _textures.FindOverlaps(range, ref _textureOverlaps);
+ int overlapCount;
+
+ _texturesLock.EnterReadLock();
+
+ try
+ {
+ overlapCount = _textures.FindOverlaps(range, ref _textureOverlaps);
+ }
+ finally
+ {
+ _texturesLock.ExitReadLock();
+ }
for (int i = 0; i < overlapCount; i++)
{
@@ -231,11 +253,20 @@ namespace Ryujinx.Graphics.Gpu.Image
}
}
- _textures.Remove(texture);
+ _texturesLock.EnterWriteLock();
- texture.ReplaceRange(range);
+ try
+ {
+ _textures.Remove(texture);
- _textures.Add(texture);
+ texture.ReplaceRange(range);
+
+ _textures.Add(texture);
+ }
+ finally
+ {
+ _texturesLock.ExitWriteLock();
+ }
return true;
}
@@ -611,11 +642,17 @@ namespace Ryujinx.Graphics.Gpu.Image
int sameAddressOverlapsCount;
- lock (_textures)
+ _texturesLock.EnterReadLock();
+
+ try
{
// Try to find a perfect texture match, with the same address and parameters.
sameAddressOverlapsCount = _textures.FindOverlaps(address, ref _textureOverlaps);
}
+ finally
+ {
+ _texturesLock.ExitReadLock();
+ }
Texture texture = null;
@@ -698,10 +735,16 @@ namespace Ryujinx.Graphics.Gpu.Image
if (info.Target != Target.TextureBuffer)
{
- lock (_textures)
+ _texturesLock.EnterReadLock();
+
+ try
{
overlapsCount = _textures.FindOverlaps(range.Value, ref _textureOverlaps);
}
+ finally
+ {
+ _texturesLock.ExitReadLock();
+ }
}
if (_overlapInfo.Length != _textureOverlaps.Length)
@@ -1025,10 +1068,16 @@ namespace Ryujinx.Graphics.Gpu.Image
_cache.Add(texture);
}
- lock (_textures)
+ _texturesLock.EnterWriteLock();
+
+ try
{
_textures.Add(texture);
}
+ finally
+ {
+ _texturesLock.ExitWriteLock();
+ }
if (partiallyMapped)
{
@@ -1091,7 +1140,19 @@ namespace Ryujinx.Graphics.Gpu.Image
return null;
}
- int addressMatches = _textures.FindOverlaps(address, ref _textureOverlaps);
+ int addressMatches;
+
+ _texturesLock.EnterReadLock();
+
+ try
+ {
+ addressMatches = _textures.FindOverlaps(address, ref _textureOverlaps);
+ }
+ finally
+ {
+ _texturesLock.ExitReadLock();
+ }
+
Texture textureMatch = null;
for (int i = 0; i < addressMatches; i++)
@@ -1232,10 +1293,16 @@ namespace Ryujinx.Graphics.Gpu.Image
/// The texture to be removed
public void RemoveTextureFromCache(Texture texture)
{
- lock (_textures)
+ _texturesLock.EnterWriteLock();
+
+ try
{
_textures.Remove(texture);
}
+ finally
+ {
+ _texturesLock.ExitWriteLock();
+ }
lock (_partiallyMappedTextures)
{
@@ -1324,13 +1391,19 @@ namespace Ryujinx.Graphics.Gpu.Image
///
public void Dispose()
{
- lock (_textures)
+ _texturesLock.EnterReadLock();
+
+ try
{
foreach (Texture texture in _textures)
{
texture.Dispose();
}
}
+ finally
+ {
+ _texturesLock.ExitReadLock();
+ }
}
}
}
From 47639e6eebe6f24bc6e0796fa27a750d9e99bd87 Mon Sep 17 00:00:00 2001
From: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com>
Date: Tue, 14 May 2024 11:36:11 -0400
Subject: [PATCH 015/109] Bump Avalonia.Svg (#6603)
* Bump Avalonia.Svg
* Remove using
* Bump Version
* Remove other reload
---
Directory.Packages.props | 4 ++--
src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs | 5 +----
src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs | 4 +---
3 files changed, 4 insertions(+), 9 deletions(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index ef274125a..d04e237e0 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -8,8 +8,8 @@
-
-
+
+
diff --git a/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs b/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs
index 5a98b1645..6b999b1f4 100644
--- a/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs
+++ b/src/Ryujinx/UI/Applet/ControllerAppletDialog.axaml.cs
@@ -9,7 +9,6 @@ using Ryujinx.Ava.UI.Windows;
using Ryujinx.Common;
using Ryujinx.HLE.HOS.Applets;
using Ryujinx.HLE.HOS.Services.Hid;
-using System;
using System.Linq;
using System.Threading.Tasks;
@@ -104,9 +103,7 @@ namespace Ryujinx.Ava.UI.Applet
if (!string.IsNullOrWhiteSpace(path))
{
- SvgSource source = new(default(Uri));
-
- source.Load(EmbeddedResources.GetStream(path));
+ SvgSource source = SvgSource.LoadFromStream(EmbeddedResources.GetStream(path));
image.Source = source;
}
diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs
index 74da45979..89cc6496d 100644
--- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs
@@ -181,9 +181,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
if (!string.IsNullOrWhiteSpace(_controllerImage))
{
- SvgSource source = new(default(Uri));
-
- source.Load(EmbeddedResources.GetStream(_controllerImage));
+ SvgSource source = SvgSource.LoadFromStream(EmbeddedResources.GetStream(_controllerImage));
image.Source = source;
}
From 2ca0b17339ada361e5f8edbf1f8dfab741e496f5 Mon Sep 17 00:00:00 2001
From: Ac_K
Date: Tue, 14 May 2024 17:47:03 +0200
Subject: [PATCH 016/109] New Crowdin updates (#6590)
* New translations en_us.json (Arabic)
* New translations en_us.json (Russian)
* New translations en_us.json (Chinese Traditional)
* New translations en_us.json (French)
* New translations en_us.json (Russian)
* New translations en_us.json (Spanish)
* New translations en_us.json (Arabic)
* New translations en_us.json (German)
* New translations en_us.json (Greek)
* New translations en_us.json (Hebrew)
* New translations en_us.json (Italian)
* New translations en_us.json (Japanese)
* New translations en_us.json (Korean)
* New translations en_us.json (Polish)
* New translations en_us.json (Russian)
* New translations en_us.json (Turkish)
* New translations en_us.json (Ukrainian)
* New translations en_us.json (Chinese Simplified)
* New translations en_us.json (Chinese Traditional)
* New translations en_us.json (Portuguese, Brazilian)
* New translations en_us.json (Thai)
* New translations en_us.json (French)
* New translations en_us.json (Arabic)
* New translations en_us.json (Italian)
* New translations en_us.json (Korean)
* New translations en_us.json (Russian)
* New translations en_us.json (Ukrainian)
* New translations en_us.json (Chinese Simplified)
* New translations en_us.json (Arabic)
* New translations en_us.json (Polish)
* New translations en_us.json (Turkish)
* New translations en_us.json (Arabic)
* New translations en_us.json (Chinese Traditional)
* New translations en_us.json (Russian)
* New translations en_us.json (French)
* New translations en_us.json (Thai)
* New translations en_us.json (Spanish)
* New translations en_us.json (Arabic)
* New translations en_us.json (German)
* New translations en_us.json (Greek)
* New translations en_us.json (Hebrew)
* New translations en_us.json (Italian)
* New translations en_us.json (Japanese)
* New translations en_us.json (Korean)
* New translations en_us.json (Polish)
* New translations en_us.json (Russian)
* New translations en_us.json (Turkish)
* New translations en_us.json (Ukrainian)
* New translations en_us.json (Chinese Simplified)
* New translations en_us.json (Chinese Traditional)
* New translations en_us.json (Portuguese, Brazilian)
* New translations en_us.json (Thai)
* New translations en_us.json (French)
* New translations en_us.json (Arabic)
* New translations en_us.json (Chinese Traditional)
* New translations en_us.json (Arabic)
* New translations en_us.json (Italian)
* New translations en_us.json (Arabic)
* New translations en_us.json (Italian)
* New translations en_us.json (Spanish)
* New translations en_us.json (Russian)
* New translations en_us.json (Russian)
* New translations en_us.json (Thai)
* New translations en_us.json (Spanish)
* New translations en_us.json (Arabic)
* New translations en_us.json (German)
* New translations en_us.json (Greek)
* New translations en_us.json (Hebrew)
* New translations en_us.json (Italian)
* New translations en_us.json (Japanese)
* New translations en_us.json (Korean)
* New translations en_us.json (Polish)
* New translations en_us.json (Russian)
* New translations en_us.json (Turkish)
* New translations en_us.json (Ukrainian)
* New translations en_us.json (Chinese Simplified)
* New translations en_us.json (Chinese Traditional)
* New translations en_us.json (Portuguese, Brazilian)
* New translations en_us.json (Thai)
* New translations en_us.json (French)
* New translations en_us.json (Chinese Traditional)
* New translations en_us.json (Arabic)
* New translations en_us.json (Russian)
* New translations en_us.json (Turkish)
* New translations en_us.json (Chinese Simplified)
* New translations en_us.json (Russian)
* New translations en_us.json (Chinese Simplified)
* New translations en_us.json (Russian)
* New translations en_us.json (Portuguese, Brazilian)
* New translations en_us.json (German)
---
src/Ryujinx/Assets/Locales/ar_SA.json | 547 +++++++++++++++-----------
src/Ryujinx/Assets/Locales/de_DE.json | 107 +++++
src/Ryujinx/Assets/Locales/el_GR.json | 107 +++++
src/Ryujinx/Assets/Locales/es_ES.json | 123 +++++-
src/Ryujinx/Assets/Locales/fr_FR.json | 113 +++++-
src/Ryujinx/Assets/Locales/he_IL.json | 107 +++++
src/Ryujinx/Assets/Locales/it_IT.json | 109 ++++-
src/Ryujinx/Assets/Locales/ja_JP.json | 107 +++++
src/Ryujinx/Assets/Locales/ko_KR.json | 107 +++++
src/Ryujinx/Assets/Locales/pl_PL.json | 119 +++++-
src/Ryujinx/Assets/Locales/pt_BR.json | 107 +++++
src/Ryujinx/Assets/Locales/ru_RU.json | 261 ++++++++----
src/Ryujinx/Assets/Locales/th_TH.json | 325 ++++++++++-----
src/Ryujinx/Assets/Locales/tr_TR.json | 135 ++++++-
src/Ryujinx/Assets/Locales/uk_UA.json | 107 +++++
src/Ryujinx/Assets/Locales/zh_CN.json | 127 +++++-
src/Ryujinx/Assets/Locales/zh_TW.json | 139 ++++++-
17 files changed, 2283 insertions(+), 464 deletions(-)
diff --git a/src/Ryujinx/Assets/Locales/ar_SA.json b/src/Ryujinx/Assets/Locales/ar_SA.json
index 51df3a052..73e55633b 100644
--- a/src/Ryujinx/Assets/Locales/ar_SA.json
+++ b/src/Ryujinx/Assets/Locales/ar_SA.json
@@ -1,38 +1,42 @@
{
- "Language": "العربية",
- "MenuBarFileOpenApplet": "فتح التطبيق المُصغَّر",
- "MenuBarFileOpenAppletOpenMiiAppletToolTip": "افتح تطبيق محرر الـMii المُصغَّر في الوضع المستقل",
+ "Language": "اَلْعَرَبِيَّةُ",
+ "MenuBarFileOpenApplet": "فتح التطبيق المصغر",
+ "MenuBarFileOpenAppletOpenMiiAppletToolTip": "افتح تطبيق تحرير Mii في الوضع المستقل",
"SettingsTabInputDirectMouseAccess": "الوصول المباشر للفأرة",
"SettingsTabSystemMemoryManagerMode": "وضع إدارة الذاكرة:",
"SettingsTabSystemMemoryManagerModeSoftware": "البرنامج",
"SettingsTabSystemMemoryManagerModeHost": "المُضيف (سريع)",
- "SettingsTabSystemMemoryManagerModeHostUnchecked": "المضيف غير محدد (سريع، غير آمن)",
- "SettingsTabSystemUseHypervisor": "استخدم الهايبرڤايزور",
+ "SettingsTabSystemMemoryManagerModeHostUnchecked": "المضيف (غير مفحوص) (أسرع، غير آمن)",
+ "SettingsTabSystemUseHypervisor": "استخدم مراقب الأجهزة الافتراضية",
"MenuBarFile": "_ملف",
"MenuBarFileOpenFromFile": "_تحميل تطبيق من ملف",
- "MenuBarFileOpenUnpacked": "تحميل لعبه غير محزومه",
- "MenuBarFileOpenEmuFolder": "فتح مجلد Ryujinx",
+ "MenuBarFileOpenUnpacked": "تحميل لُعْبَة غير محزومة",
+ "MenuBarFileOpenEmuFolder": "فتح مجلد Ryujinx",
"MenuBarFileOpenLogsFolder": "فتح مجلد السجلات",
"MenuBarFileExit": "_خروج",
"MenuBarOptions": "_خيارات",
- "MenuBarOptionsToggleFullscreen": "وضع ملء الشاشة",
+ "MenuBarOptionsToggleFullscreen": "التبديل إلى وضع ملء الشاشة",
"MenuBarOptionsStartGamesInFullscreen": "ابدأ الألعاب في وضع ملء الشاشة",
"MenuBarOptionsStopEmulation": "إيقاف المحاكاة",
- "MenuBarOptionsSettings": "الإعدادات",
- "MenuBarOptionsManageUserProfiles": "إدارة الملفات الشخصية للمستخدم",
- "MenuBarActions": "الإجراءات",
+ "MenuBarOptionsSettings": "_الإعدادات",
+ "MenuBarOptionsManageUserProfiles": "_إدارة الملفات الشخصية للمستخدم",
+ "MenuBarActions": "_الإجراءات",
"MenuBarOptionsSimulateWakeUpMessage": "محاكاة رسالة الاستيقاظ",
- "MenuBarActionsScanAmiibo": "فحص Amiibo",
- "MenuBarTools": "الأدوات",
- "MenuBarToolsInstallFirmware": "تثبيت البرامج الثابتة",
- "MenuBarFileToolsInstallFirmwareFromFile": "تثبيت البرنامج الثابت من XCI أو ZIP",
+ "MenuBarActionsScanAmiibo": "فحص Amiibo",
+ "MenuBarTools": "_الأدوات",
+ "MenuBarToolsInstallFirmware": "تثبيت البرنامج الثابت",
+ "MenuBarFileToolsInstallFirmwareFromFile": "تثبيت برنامج ثابت من XCI أو ZIP",
"MenuBarFileToolsInstallFirmwareFromDirectory": "تثبيت برنامج ثابت من مجلد",
"MenuBarToolsManageFileTypes": "إدارة أنواع الملفات",
"MenuBarToolsInstallFileTypes": "تثبيت أنواع الملفات",
"MenuBarToolsUninstallFileTypes": "إزالة أنواع الملفات",
+ "MenuBarView": "_عرض",
+ "MenuBarViewWindow": "حجم النافذة",
+ "MenuBarViewWindow720": "720p",
+ "MenuBarViewWindow1080": "1080p",
"MenuBarHelp": "_مساعدة",
"MenuBarHelpCheckForUpdates": "تحقق من التحديثات",
- "MenuBarHelpAbout": "عن البرنامج",
+ "MenuBarHelpAbout": "حول",
"MenuSearch": "بحث...",
"GameListHeaderFavorite": "مفضلة",
"GameListHeaderIcon": "الأيقونة",
@@ -40,62 +44,63 @@
"GameListHeaderDeveloper": "المطور",
"GameListHeaderVersion": "الإصدار",
"GameListHeaderTimePlayed": "وقت اللعب",
- "GameListHeaderLastPlayed": "اخر تشغيل",
- "GameListHeaderFileExtension": "امتداد الملف",
+ "GameListHeaderLastPlayed": "آخر مرة لُعبت",
+ "GameListHeaderFileExtension": "صيغة الملف",
"GameListHeaderFileSize": "حجم الملف",
"GameListHeaderPath": "المسار",
"GameListContextMenuOpenUserSaveDirectory": "فتح مجلد حفظ المستخدم",
"GameListContextMenuOpenUserSaveDirectoryToolTip": "يفتح المجلد الذي يحتوي على حفظ المستخدم للتطبيق",
"GameListContextMenuOpenDeviceSaveDirectory": "فتح مجلد حفظ الجهاز",
"GameListContextMenuOpenDeviceSaveDirectoryToolTip": "يفتح المجلد الذي يحتوي على حفظ الجهاز للتطبيق",
- "GameListContextMenuOpenBcatSaveDirectory": "فتح مجلد حفظ الـBCAT",
- "GameListContextMenuOpenBcatSaveDirectoryToolTip": "يفتح المجلد الذي يحتوي على حفظ الـBCAT للتطبيق",
- "GameListContextMenuManageTitleUpdates": "إدارة تحديثات العنوان",
- "GameListContextMenuManageTitleUpdatesToolTip": "يفتح نافذة إدارة تحديث العنوان",
+ "GameListContextMenuOpenBcatSaveDirectory": "فتح مجلد حفظ الـBCAT",
+ "GameListContextMenuOpenBcatSaveDirectoryToolTip": "يفتح المجلد الذي يحتوي على حفظ الـBCAT للتطبيق",
+ "GameListContextMenuManageTitleUpdates": "إدارة تحديثات اللُعبة",
+ "GameListContextMenuManageTitleUpdatesToolTip": "يفتح نافذة إدارة تحديث اللُعبة",
"GameListContextMenuManageDlc": "إدارة المحتوي الإضافي",
"GameListContextMenuManageDlcToolTip": "يفتح نافذة إدارة المحتوي الإضافي",
"GameListContextMenuCacheManagement": "إدارة ذاكرة التخزين المؤقت",
- "GameListContextMenuCacheManagementPurgePptc": "إعادة بناء PPTC في قائمة الانتظار",
- "GameListContextMenuCacheManagementPurgePptcToolTip": "تنشيط PPTC لإعادة البناء في وقت التمهيد عند بدء تشغيل اللعبة التالية",
- "GameListContextMenuCacheManagementPurgeShaderCache": "إزالة ذاكرة التشغيل المؤقتة للمظللات ",
- "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "حذف الذاكرة المؤقتة للمظللات الخاصة بالتطبيق",
- "GameListContextMenuCacheManagementOpenPptcDirectory": "فتح مجلد PPTC",
- "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "يفتح المجلد الذي يحتوي على الـPPTC للتطبيق",
- "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "فتح مجلد الذاكرة المؤقتة للمظللات ",
- "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "يفتح المجلد الذي يحتوي على ذاكرة التشغيل المؤقتة للمظللات الخاصة بالتطبيق",
- "GameListContextMenuExtractData": "إستخراج البيانات",
+ "GameListContextMenuCacheManagementPurgePptc": "قائمة انتظار إعادة بناء الـPPTC",
+ "GameListContextMenuCacheManagementPurgePptcToolTip": "تنشيط PPTC لإعادة البناء في وقت الإقلاع عند بدء تشغيل اللعبة التالي",
+ "GameListContextMenuCacheManagementPurgeShaderCache": "تنظيف ذاكرة مرشحات الفيديو المؤقتة",
+ "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "يحذف ذاكرة مرشحات الفيديو المؤقتة الخاصة بالتطبيق",
+ "GameListContextMenuCacheManagementOpenPptcDirectory": "فتح مجلد PPTC",
+ "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "يفتح المجلد الذي يحتوي على ذاكرة التخزين المؤقت للترجمة المستمرة (PPTC) للتطبيق",
+ "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "فتح مجلد الذاكرة المؤقتة لمرشحات الفيديو ",
+ "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "يفتح المجلد الذي يحتوي على ذاكرة المظللات المؤقتة للتطبيق",
+ "GameListContextMenuExtractData": "استخراج البيانات",
"GameListContextMenuExtractDataExeFS": "ExeFS",
- "GameListContextMenuExtractDataExeFSToolTip": "إستخراج قسم ExeFS من التكوين الحالي للتطبيقات (بما في ذلك التحديثات)",
+ "GameListContextMenuExtractDataExeFSToolTip": " استخراج قسم نظام الملفات القابل للتنفيذ (ExeFS) من الإعدادات الحالية للتطبيقات (يتضمن التحديثات)",
"GameListContextMenuExtractDataRomFS": "RomFS",
- "GameListContextMenuExtractDataRomFSToolTip": "استخراج قسم RomFS من التكوين الحالي للتطبيقات (بما في ذلك التحديثات)",
+ "GameListContextMenuExtractDataRomFSToolTip": "استخراج قسم RomFS من الإعدادات الحالية للتطبيقات (يتضمن التحديثات)",
"GameListContextMenuExtractDataLogo": "شعار",
- "GameListContextMenuExtractDataLogoToolTip": "استخراج قسم الشعار من التكوين الحالي للتطبيقات (بما في ذلك التحديثات)",
+ "GameListContextMenuExtractDataLogoToolTip": "استخراج قسم الشعار من الإعدادات الحالية للتطبيقات (يتضمن التحديثات)",
"GameListContextMenuCreateShortcut": "إنشاء اختصار للتطبيق",
- "GameListContextMenuCreateShortcutToolTip": "قم بإنشاء اختصار لسطح المكتب لتشغيل التطبيق المحدد",
- "GameListContextMenuCreateShortcutToolTipMacOS": "قم بإنشاء اختصار في مجلد تطبيقات نظام التشغيل MacOS الذي يقوم بتشغيل التطبيق المحدد",
- "GameListContextMenuOpenModsDirectory": "فتح مجلد التعديلات",
- "GameListContextMenuOpenModsDirectoryToolTip": "يفتح المجلد الذي يحتوي على تعديلات التطبيق",
- "GameListContextMenuOpenSdModsDirectory": "فتح مجلد تعديلات Atmosphere",
- "GameListContextMenuOpenSdModsDirectoryToolTip": "يفتح دليل Atmosphere لبطاقة SD البديلة الذي يحتوي على تعديلات التطبيق. مفيد للتعديلات التي تم تعبئتها للأجهزة الحقيقية.",
- "StatusBarGamesLoaded": "{0}/{1} الألعاب التي تم تحميلها",
+ "GameListContextMenuCreateShortcutToolTip": "أنشئ اختصار سطح مكتب لتشغيل التطبيق المحدد",
+ "GameListContextMenuCreateShortcutToolTipMacOS": "أنشئ اختصار يُشغل التطبيق المحدد في مجلد تطبيقات macOS",
+ "GameListContextMenuOpenModsDirectory": "فتح مجلد التعديلات (Mods)",
+ "GameListContextMenuOpenModsDirectoryToolTip": "يفتح المجلد الذي يحتوي على تعديلات(mods) التطبيق",
+ "GameListContextMenuOpenSdModsDirectory": "فتح مجلد تعديلات(mods) أتموسفير",
+ "GameListContextMenuOpenSdModsDirectoryToolTip": "يفتح مجلد أتموسفير لبطاقة SD البديلة الذي يحتوي على تعديلات التطبيق. مفيد للتعديلات التي تم تعبئتها للأجهزة الحقيقية.",
+ "StatusBarGamesLoaded": "{0}/{1} لعبة تم تحميلها",
"StatusBarSystemVersion": "إصدار النظام: {0}",
"LinuxVmMaxMapCountDialogTitle": "الحد الأدنى لتعيينات الذاكرة المكتشفة",
"LinuxVmMaxMapCountDialogTextPrimary": "هل ترغب في زيادة قيمة vm.max_map_count إلى {0}",
- "LinuxVmMaxMapCountDialogTextSecondary": "قد تحاول بعض الألعاب إنشاء المزيد من تعيينات الذاكرة أكثر مما هو مسموح به حاليا. سيتحطم Ryujinx بمجرد تجاوز هذا الحد.",
+ "LinuxVmMaxMapCountDialogTextSecondary": "قد تحاول بعض الألعاب إنشاء المزيد من تعيينات الذاكرة أكثر مما هو مسموح به حاليا. سيغلق ريوجينكس بمجرد تجاوز هذا الحد.",
"LinuxVmMaxMapCountDialogButtonUntilRestart": "نعم، حتى إعادة التشغيل التالية",
- "LinuxVmMaxMapCountDialogButtonPersistent": "نعم، بشكل دائم",
+ "LinuxVmMaxMapCountDialogButtonPersistent": "نعم، دائمًا",
"LinuxVmMaxMapCountWarningTextPrimary": "الحد الأقصى لمقدار تعيينات الذاكرة أقل من الموصى به.",
- "LinuxVmMaxMapCountWarningTextSecondary": "القيمة الحالية لـ vm.max_map_count ({0}) أقل من {1}. قد تحاول بعض الألعاب إنشاء المزيد من تعيينات الذاكرة أكثر مما هو مسموح به حاليا. سيتحطم Ryujinx بمجرد تجاوز هذا الحد.\n\nقد ترغب في زيادة الحد اليدوي أو تثبيت pkexec، مما يسمح لـ Ryujinx بالمساعدة في ذلك.",
+ "LinuxVmMaxMapCountWarningTextSecondary": "القيمة الحالية لـ vm.max_map_count ({0}) أقل من {1}. قد تحاول بعض الألعاب إنشاء المزيد من تعيينات الذاكرة أكثر مما هو مسموح به حاليا. سيغلق ريوجينكس بمجرد تجاوز هذا الحد.\n\nقد ترغب إما في زيادة الحد يدويا أو تثبيت pkexec، مما يسمح لـ ريوجينكس بالمساعدة في ذلك.",
"Settings": "إعدادات",
"SettingsTabGeneral": "واجهة المستخدم",
- "SettingsTabGeneralGeneral": "العامة",
+ "SettingsTabGeneralGeneral": "عام",
"SettingsTabGeneralEnableDiscordRichPresence": "تمكين وجود ديسكورد الغني",
"SettingsTabGeneralCheckUpdatesOnLaunch": "التحقق من وجود تحديثات عند التشغيل",
"SettingsTabGeneralShowConfirmExitDialog": "إظهار مربع حوار \"تأكيد الخروج\"",
+ "SettingsTabGeneralRememberWindowState": "تذكر حجم/موضع النافذة",
"SettingsTabGeneralHideCursor": "إخفاء المؤشر:",
- "SettingsTabGeneralHideCursorNever": "مطلقاً",
+ "SettingsTabGeneralHideCursorNever": "مطلقا",
"SettingsTabGeneralHideCursorOnIdle": "عند الخمول",
- "SettingsTabGeneralHideCursorAlways": "دائماً",
+ "SettingsTabGeneralHideCursorAlways": "دائما",
"SettingsTabGeneralGameDirectories": "مجلدات الألعاب",
"SettingsTabGeneralAdd": "إضافة",
"SettingsTabGeneralRemove": "إزالة",
@@ -127,24 +132,24 @@
"SettingsTabSystemSystemLanguageLatinAmericanSpanish": "إسبانية أمريكا اللاتينية",
"SettingsTabSystemSystemLanguageSimplifiedChinese": "الصينية المبسطة",
"SettingsTabSystemSystemLanguageTraditionalChinese": "الصينية التقليدية",
- "SettingsTabSystemSystemTimeZone": "نظام التوقيت للنظام:",
+ "SettingsTabSystemSystemTimeZone": "النطاق الزمني للنظام:",
"SettingsTabSystemSystemTime": "توقيت النظام:",
"SettingsTabSystemEnableVsync": "VSync",
- "SettingsTabSystemEnablePptc": "PPTC (Profiled Persistent Translation Cache)",
- "SettingsTabSystemEnableFsIntegrityChecks": "FS Integrity Checks",
+ "SettingsTabSystemEnablePptc": "PPTC (ذاكرة التخزين المؤقت للترجمة المستمرة)",
+ "SettingsTabSystemEnableFsIntegrityChecks": "التحقق من سلامة نظام الملفات",
"SettingsTabSystemAudioBackend": "خلفية الصوت:",
"SettingsTabSystemAudioBackendDummy": "زائف",
"SettingsTabSystemAudioBackendOpenAL": "OpenAL",
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
"SettingsTabSystemAudioBackendSDL2": "SDL2",
- "SettingsTabSystemHacks": "الاختراقات",
+ "SettingsTabSystemHacks": "هاكات",
"SettingsTabSystemHacksNote": "قد يتسبب في عدم الاستقرار",
"SettingsTabSystemExpandDramSize": "استخدام تخطيط الذاكرة البديل (المطورين)",
"SettingsTabSystemIgnoreMissingServices": "تجاهل الخدمات المفقودة",
"SettingsTabGraphics": "الرسومات",
- "SettingsTabGraphicsAPI": "الرسومات API",
+ "SettingsTabGraphicsAPI": "API الرسومات ",
"SettingsTabGraphicsEnableShaderCache": "تفعيل ذاكرة المظللات المؤقتة",
- "SettingsTabGraphicsAnisotropicFiltering": "تصفية متباين الخواص:",
+ "SettingsTabGraphicsAnisotropicFiltering": "تصفية:",
"SettingsTabGraphicsAnisotropicFilteringAuto": "تلقائي",
"SettingsTabGraphicsAnisotropicFiltering2x": "2x",
"SettingsTabGraphicsAnisotropicFiltering4x": "4×",
@@ -152,7 +157,7 @@
"SettingsTabGraphicsAnisotropicFiltering16x": "16x",
"SettingsTabGraphicsResolutionScale": "مقياس الدقة",
"SettingsTabGraphicsResolutionScaleCustom": "مخصص (لا ينصح به)",
- "SettingsTabGraphicsResolutionScaleNative": "الأصل (720p/1080p)",
+ "SettingsTabGraphicsResolutionScaleNative": "الأصل (720p/1080p)",
"SettingsTabGraphicsResolutionScale2x": "2x (1440p/2160p)",
"SettingsTabGraphicsResolutionScale3x": "3x (2160p/3240p)",
"SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p) (لا ينصح به)",
@@ -162,11 +167,11 @@
"SettingsTabGraphicsAspectRatio16x10": "16:10",
"SettingsTabGraphicsAspectRatio21x9": "21:9",
"SettingsTabGraphicsAspectRatio32x9": "32:9",
- "SettingsTabGraphicsAspectRatioStretch": "تمدد لتلائم حجم النافذة",
+ "SettingsTabGraphicsAspectRatioStretch": "تمديد لتناسب النافذة",
"SettingsTabGraphicsDeveloperOptions": "خيارات المطور",
- "SettingsTabGraphicsShaderDumpPath": "مسار تفريغ الرسومات:",
- "SettingsTabLogging": "التسجيل",
- "SettingsTabLoggingLogging": "التسجيل",
+ "SettingsTabGraphicsShaderDumpPath": "مسار تفريغ المظللات:",
+ "SettingsTabLogging": "تسجيل",
+ "SettingsTabLoggingLogging": "تسجيل",
"SettingsTabLoggingEnableLoggingToFile": "تفعيل التسجيل إلى ملف",
"SettingsTabLoggingEnableStubLogs": "تفعيل سجلات الـStub",
"SettingsTabLoggingEnableInfoLogs": "تفعيل سجلات المعلومات",
@@ -174,8 +179,8 @@
"SettingsTabLoggingEnableErrorLogs": "تفعيل سجلات الأخطاء",
"SettingsTabLoggingEnableTraceLogs": "تفعيل سجلات التتبع",
"SettingsTabLoggingEnableGuestLogs": "تفعيل سجلات الضيوف",
- "SettingsTabLoggingEnableFsAccessLogs": "تمكين سجلات الوصول لـ Fs",
- "SettingsTabLoggingFsGlobalAccessLogMode": "وضع سجل وصول عالمي Fs :",
+ "SettingsTabLoggingEnableFsAccessLogs": "تمكين سجلات الوصول إلى نظام الملفات",
+ "SettingsTabLoggingFsGlobalAccessLogMode": "وضع سجل الوصول العالمي لنظام الملفات:",
"SettingsTabLoggingDeveloperOptions": "خيارات المطور",
"SettingsTabLoggingDeveloperOptionsNote": "تحذير: سوف يقلل من الأداء",
"SettingsTabLoggingGraphicsBackendLogLevel": "مستوى سجل خلفية الرسومات:",
@@ -185,15 +190,15 @@
"SettingsTabLoggingGraphicsBackendLogLevelAll": "الكل",
"SettingsTabLoggingEnableDebugLogs": "تمكين سجلات التصحيح",
"SettingsTabInput": "الإدخال",
- "SettingsTabInputEnableDockedMode": "مركب بالمنصة",
- "SettingsTabInputDirectKeyboardAccess": "الوصول المباشر إلى لوحة المفاتيح",
+ "SettingsTabInputEnableDockedMode": "تركيب بالمنصة",
+ "SettingsTabInputDirectKeyboardAccess": "الوصول المباشر للوحة المفاتيح",
"SettingsButtonSave": "حفظ",
"SettingsButtonClose": "إغلاق",
"SettingsButtonOk": "موافق",
"SettingsButtonCancel": "إلغاء",
"SettingsButtonApply": "تطبيق",
"ControllerSettingsPlayer": "اللاعب",
- "ControllerSettingsPlayer1": "اللاعب ١",
+ "ControllerSettingsPlayer1": "اللاعب 1",
"ControllerSettingsPlayer2": "اللاعب 2",
"ControllerSettingsPlayer3": "اللاعب 3",
"ControllerSettingsPlayer4": "اللاعب 4",
@@ -205,12 +210,12 @@
"ControllerSettingsInputDevice": "جهاز الإدخال",
"ControllerSettingsRefresh": "تحديث",
"ControllerSettingsDeviceDisabled": "معطل",
- "ControllerSettingsControllerType": "نوع ذراع التحكم",
+ "ControllerSettingsControllerType": "نوع وحدة التحكم",
"ControllerSettingsControllerTypeHandheld": "محمول",
- "ControllerSettingsControllerTypeProController": "Pro Controller",
- "ControllerSettingsControllerTypeJoyConPair": "اقتران جوي كون",
- "ControllerSettingsControllerTypeJoyConLeft": "يسار جوي كون",
- "ControllerSettingsControllerTypeJoyConRight": "يمين جوي كون",
+ "ControllerSettingsControllerTypeProController": "وحدة تحكم برو",
+ "ControllerSettingsControllerTypeJoyConPair": "زوج جوي كون",
+ "ControllerSettingsControllerTypeJoyConLeft": "جوي كون اليسار ",
+ "ControllerSettingsControllerTypeJoyConRight": " جوي كون اليمين",
"ControllerSettingsProfile": "الملف الشخصي",
"ControllerSettingsProfileDefault": "افتراضي",
"ControllerSettingsLoad": "تحميل",
@@ -223,13 +228,13 @@
"ControllerSettingsButtonY": "Y",
"ControllerSettingsButtonPlus": "+",
"ControllerSettingsButtonMinus": "-",
- "ControllerSettingsDPad": "لوحة الاتجاه",
+ "ControllerSettingsDPad": "أسهم الاتجاهات",
"ControllerSettingsDPadUp": "اعلى",
"ControllerSettingsDPadDown": "أسفل",
"ControllerSettingsDPadLeft": "يسار",
"ControllerSettingsDPadRight": "يمين",
"ControllerSettingsStickButton": "زر",
- "ControllerSettingsStickUp": "اعلى",
+ "ControllerSettingsStickUp": "فوق",
"ControllerSettingsStickDown": "أسفل",
"ControllerSettingsStickLeft": "يسار",
"ControllerSettingsStickRight": "يمين",
@@ -239,11 +244,11 @@
"ControllerSettingsStickDeadzone": "المنطقة الميتة:",
"ControllerSettingsLStick": "العصا اليسرى",
"ControllerSettingsRStick": "العصا اليمنى",
- "ControllerSettingsTriggersLeft": "المحفزات اليسرى",
- "ControllerSettingsTriggersRight": "المحفزات اليمني",
- "ControllerSettingsTriggersButtonsLeft": "أزرار التحفيز اليسرى",
- "ControllerSettingsTriggersButtonsRight": "أزرار التحفيز اليمنى",
- "ControllerSettingsTriggers": "المحفزات",
+ "ControllerSettingsTriggersLeft": "الأزندة اليسرى",
+ "ControllerSettingsTriggersRight": "الأزندة اليمني",
+ "ControllerSettingsTriggersButtonsLeft": "أزرار الزناد اليسرى",
+ "ControllerSettingsTriggersButtonsRight": "أزرار الزناد اليمنى",
+ "ControllerSettingsTriggers": "أزندة",
"ControllerSettingsTriggerL": "L",
"ControllerSettingsTriggerR": "R",
"ControllerSettingsTriggerZL": "ZL",
@@ -258,27 +263,128 @@
"ControllerSettingsTriggerThreshold": "قوة التحفيز:",
"ControllerSettingsMotion": "الحركة",
"ControllerSettingsMotionUseCemuhookCompatibleMotion": "استخدام الحركة المتوافقة مع CemuHook",
- "ControllerSettingsMotionControllerSlot": "خانة ذراع التحكم:",
+ "ControllerSettingsMotionControllerSlot": "خانة وحدة التحكم:",
"ControllerSettingsMotionMirrorInput": "إعادة الإدخال",
- "ControllerSettingsMotionRightJoyConSlot": "خانة اليمين JoyCon :",
+ "ControllerSettingsMotionRightJoyConSlot": "خانة جويكون اليمين :",
"ControllerSettingsMotionServerHost": "مضيف الخادم:",
- "ControllerSettingsMotionGyroSensitivity": "حساسية الغيرو:",
- "ControllerSettingsMotionGyroDeadzone": "منطقة الغيرو الميتة:",
+ "ControllerSettingsMotionGyroSensitivity": "حساسية مستشعر الحركة:",
+ "ControllerSettingsMotionGyroDeadzone": "منطقة مستشعر الحركة الميتة:",
"ControllerSettingsSave": "حفظ",
"ControllerSettingsClose": "إغلاق",
+ "KeyUnknown": "مجهول",
+ "KeyShiftLeft": "زر Shift الأيسر",
+ "KeyShiftRight": "زر Shift الأيمن",
+ "KeyControlLeft": "زر Ctrl الأيسر",
+ "KeyMacControlLeft": "زر ⌃ الأيسر",
+ "KeyControlRight": "زر Ctrl الأيمن",
+ "KeyMacControlRight": "زر ⌃ الأيمن",
+ "KeyAltLeft": "زر Alt الأيسر",
+ "KeyMacAltLeft": "زر ⌥ الأيسر",
+ "KeyAltRight": "زر Alt الأيمن",
+ "KeyMacAltRight": "زر ⌥ الأيمن",
+ "KeyWinLeft": "زر ⊞ الأيسر",
+ "KeyMacWinLeft": "زر ⌘ الأيسر",
+ "KeyWinRight": "زر ⊞ الأيمن",
+ "KeyMacWinRight": "زر ⌘ الأيمن",
+ "KeyMenu": "زر القائمة",
+ "KeyUp": "فوق",
+ "KeyDown": "اسفل",
+ "KeyLeft": "يسار",
+ "KeyRight": "يمين",
+ "KeyEnter": "مفتاح الإدخال",
+ "KeyEscape": "زر Escape",
+ "KeySpace": "مسافة",
+ "KeyTab": "زر Tab",
+ "KeyBackSpace": "زر المسح للخلف",
+ "KeyInsert": "زر Insert",
+ "KeyDelete": "زر الحذف",
+ "KeyPageUp": "زر Page Up",
+ "KeyPageDown": "زر Page Down",
+ "KeyHome": "زر Home",
+ "KeyEnd": "زر End",
+ "KeyCapsLock": "زر الحروف الكبيرة",
+ "KeyScrollLock": "زر Scroll Lock",
+ "KeyPrintScreen": "زر Print Screen",
+ "KeyPause": "زر Pause",
+ "KeyNumLock": "زر Num Lock",
+ "KeyClear": "زر Clear",
+ "KeyKeypad0": "لوحة الأرقام 0",
+ "KeyKeypad1": "لوحة الأرقام 1",
+ "KeyKeypad2": "لوحة الأرقام 2",
+ "KeyKeypad3": "لوحة الأرقام 3",
+ "KeyKeypad4": "لوحة الأرقام 4",
+ "KeyKeypad5": "لوحة الأرقام 5",
+ "KeyKeypad6": "لوحة الأرقام 6",
+ "KeyKeypad7": "لوحة الأرقام 7",
+ "KeyKeypad8": "لوحة الأرقام 8",
+ "KeyKeypad9": "لوحة الأرقام 9",
+ "KeyKeypadDivide": "لوحة الأرقام علامة القسمة",
+ "KeyKeypadMultiply": "لوحة الأرقام علامة الضرب",
+ "KeyKeypadSubtract": "لوحة الأرقام علامة الطرح\n",
+ "KeyKeypadAdd": "لوحة الأرقام علامة الزائد",
+ "KeyKeypadDecimal": "لوحة الأرقام الفاصلة العشرية",
+ "KeyKeypadEnter": "لوحة الأرقام زر الإدخال",
+ "KeyNumber0": "٠",
+ "KeyNumber1": "١",
+ "KeyNumber2": "٢",
+ "KeyNumber3": "٣",
+ "KeyNumber4": "٤",
+ "KeyNumber5": "٥",
+ "KeyNumber6": "٦",
+ "KeyNumber7": "٧",
+ "KeyNumber8": "٨",
+ "KeyNumber9": "٩",
+ "KeyTilde": "~",
+ "KeyGrave": "`",
+ "KeyMinus": "-",
+ "KeyPlus": "+",
+ "KeyBracketLeft": "[",
+ "KeyBracketRight": "]",
+ "KeySemicolon": ";",
+ "KeyQuote": "\"",
+ "KeyComma": ",",
+ "KeyPeriod": ".",
+ "KeySlash": "/",
+ "KeyBackSlash": "\\",
+ "KeyUnbound": "غير مرتبط",
+ "GamepadLeftStick": "زر عصا التحكم اليسرى",
+ "GamepadRightStick": "زر عصا التحكم اليمنى",
+ "GamepadLeftShoulder": "زر الكتف الأيسر L",
+ "GamepadRightShoulder": "زر الكتف الأيمن R",
+ "GamepadLeftTrigger": "زر الزناد الأيسر (ZL)",
+ "GamepadRightTrigger": "زر الزناد الأيمن (ZR)",
+ "GamepadDpadUp": "فوق",
+ "GamepadDpadDown": "اسفل",
+ "GamepadDpadLeft": "يسار",
+ "GamepadDpadRight": "يمين",
+ "GamepadMinus": "-",
+ "GamepadPlus": "+",
+ "GamepadGuide": "دليل",
+ "GamepadMisc1": "متنوع",
+ "GamepadPaddle1": "Paddle 1",
+ "GamepadPaddle2": "Paddle 2",
+ "GamepadPaddle3": "Paddle 3",
+ "GamepadPaddle4": "Paddle 4",
+ "GamepadTouchpad": "لوحة اللمس",
+ "GamepadSingleLeftTrigger0": "زر الزناد الأيسر 0",
+ "GamepadSingleRightTrigger0": "زر الزناد الأيمن 0",
+ "GamepadSingleLeftTrigger1": "زر الزناد الأيسر 1",
+ "GamepadSingleRightTrigger1": "زر الزناد الأيمن 1",
+ "StickLeft": "عصا التحكم اليسرى",
+ "StickRight": "عصا التحكم اليمنى",
"UserProfilesSelectedUserProfile": "الملف الشخصي المحدد للمستخدم:",
"UserProfilesSaveProfileName": "حفظ اسم الملف الشخصي",
"UserProfilesChangeProfileImage": "تغيير صورة الملف الشخصي",
"UserProfilesAvailableUserProfiles": "الملفات الشخصية للمستخدم المتاحة:",
- "UserProfilesAddNewProfile": "أنشئ ملف شخصي",
+ "UserProfilesAddNewProfile": "إنشاء ملف الشخصي",
"UserProfilesDelete": "حذف",
"UserProfilesClose": "إغلاق",
"ProfileNameSelectionWatermark": "اختر اسم مستعار",
"ProfileImageSelectionTitle": "تحديد صورة الملف الشخصي",
"ProfileImageSelectionHeader": "اختر صورة الملف الشخصي",
- "ProfileImageSelectionNote": "يمكنك استيراد صورة ملف تعريف مخصصة، أو تحديد صورة رمزية من البرامج الثابتة للنظام",
+ "ProfileImageSelectionNote": "يمكنك استيراد صورة ملف شخصي مخصصة، أو تحديد صورة رمزية من البرامج الثابتة للنظام",
"ProfileImageSelectionImportImage": "استيراد ملف الصورة",
- "ProfileImageSelectionSelectAvatar": "حدد الصورة الرمزية للبرنامج الثابت",
+ "ProfileImageSelectionSelectAvatar": "حدد الصورة الرمزية من البرنامج الثابتة",
"InputDialogTitle": "حوار الإدخال",
"InputDialogOk": "موافق",
"InputDialogCancel": "إلغاء",
@@ -289,13 +395,13 @@
"AvatarSetBackgroundColor": "تعيين لون الخلفية",
"AvatarClose": "إغلاق",
"ControllerSettingsLoadProfileToolTip": "تحميل الملف الشخصي",
- "ControllerSettingsAddProfileToolTip": "إضافة ملف تعريف",
- "ControllerSettingsRemoveProfileToolTip": "إزالة ملف التعريف",
- "ControllerSettingsSaveProfileToolTip": "حفظ ملف التعريف",
+ "ControllerSettingsAddProfileToolTip": "إضافة ملف شخصي",
+ "ControllerSettingsRemoveProfileToolTip": "إزالة الملف الشخصي",
+ "ControllerSettingsSaveProfileToolTip": "حفظ الملف الشخصي",
"MenuBarFileToolsTakeScreenshot": "أخذ لقطة للشاشة",
"MenuBarFileToolsHideUi": "إخفاء واجهة المستخدم",
"GameListContextMenuRunApplication": "تشغيل التطبيق",
- "GameListContextMenuToggleFavorite": "تبديل المفضلة",
+ "GameListContextMenuToggleFavorite": "تعيين كمفضل",
"GameListContextMenuToggleFavoriteToolTip": "تبديل الحالة المفضلة للعبة",
"SettingsTabGeneralTheme": "السمة:",
"SettingsTabGeneralThemeDark": "داكن",
@@ -304,44 +410,44 @@
"ControllerSettingsRumble": "الاهتزاز",
"ControllerSettingsRumbleStrongMultiplier": "مضاعف اهتزاز قوي",
"ControllerSettingsRumbleWeakMultiplier": "مضاعف اهتزاز ضعيف",
- "DialogMessageSaveNotAvailableMessage": "لا يوجد حفظ لـ {0} [{1:x16}]",
- "DialogMessageSaveNotAvailableCreateSaveMessage": "هل ترغب في إنشاء حفظ لهذه اللعبة؟",
- "DialogConfirmationTitle": "Ryujinx - تأكيد",
- "DialogUpdaterTitle": "تحديث Ryujinx",
- "DialogErrorTitle": "Ryujinx - خطأ",
- "DialogWarningTitle": "Ryujinx - تحذير",
- "DialogExitTitle": "Ryujinx - الخروج",
- "DialogErrorMessage": "واجه Ryujinx خطأ",
- "DialogExitMessage": "هل أنت متأكد من أنك تريد إغلاق Ryujinx؟",
+ "DialogMessageSaveNotAvailableMessage": "لا توجد بيانات الحفظ لـ {0} [{1:x16}]",
+ "DialogMessageSaveNotAvailableCreateSaveMessage": "هل ترغب في إنشاء بيانات الحفظ لهذه اللعبة؟",
+ "DialogConfirmationTitle": "ريوجينكس - تأكيد",
+ "DialogUpdaterTitle": "ريوجينكس - المحدث",
+ "DialogErrorTitle": "ريوجينكس - خطأ",
+ "DialogWarningTitle": "ريوجينكس - تحذير",
+ "DialogExitTitle": "ريوجينكس - الخروج",
+ "DialogErrorMessage": "واجه ريوجينكس خطأ",
+ "DialogExitMessage": "هل أنت متأكد من أنك تريد إغلاق ريوجينكس؟",
"DialogExitSubMessage": "سيتم فقدان كافة البيانات غير المحفوظة!",
- "DialogMessageCreateSaveErrorMessage": "حدث خطأ أثناء إنشاء المحفوظة المحددة: {0}",
- "DialogMessageFindSaveErrorMessage": "حدث خطأ أثناء البحث عن البيانات المحفوظة المحددة: {0}",
+ "DialogMessageCreateSaveErrorMessage": "حدث خطأ أثناء إنشاء بيانات الحفظ المحددة: {0}",
+ "DialogMessageFindSaveErrorMessage": "حدث خطأ أثناء البحث عن بيانات الحفظ المحددة: {0}",
"FolderDialogExtractTitle": "اختر المجلد الذي تريد الاستخراج إليه",
"DialogNcaExtractionMessage": "استخراج قسم {0} من {1}...",
- "DialogNcaExtractionTitle": "Ryujinx - مستخرج قسم NCA",
- "DialogNcaExtractionMainNcaNotFoundErrorMessage": "فشل الاستخراج. لم يكن NCA الرئيسي موجودًا في الملف المحدد.",
- "DialogNcaExtractionCheckLogErrorMessage": "فشل الاستخراج. اقرأ ملف السجل لمزيد من المعلومات.",
+ "DialogNcaExtractionTitle": "ريوجينكس - مستخرج قسم NCA",
+ "DialogNcaExtractionMainNcaNotFoundErrorMessage": "فشل الاستخراج. لم يكن NCA الرئيسي موجودا في الملف المحدد.",
+ "DialogNcaExtractionCheckLogErrorMessage": "فشل الاستخراج. اقرأ ملف التسجيل لمزيد من المعلومات.",
"DialogNcaExtractionSuccessMessage": "تم الاستخراج بنجاح.",
- "DialogUpdaterConvertFailedMessage": "فشل تحويل إصدار Ryujinx الحالي.",
+ "DialogUpdaterConvertFailedMessage": "فشل تحويل إصدار ريوجينكس الحالي.",
"DialogUpdaterCancelUpdateMessage": "إلغاء التحديث",
- "DialogUpdaterAlreadyOnLatestVersionMessage": "أنت تستخدم بالفعل أحدث إصدار من Ryujinx!",
- "DialogUpdaterFailedToGetVersionMessage": "حدث خطأ أثناء محاولة الحصول على معلومات الإصدار من إصدار GitHub. يمكن أن يحدث هذا إذا تم تجميع إصدار جديد بواسطة GitHub Actions. جرب مجددا بعد دقائق.",
- "DialogUpdaterConvertFailedGithubMessage": "فشل تحويل إصدار Ryujinx المستلم من إصدار Github.",
- "DialogUpdaterDownloadingMessage": "تحميل التحديث...",
- "DialogUpdaterExtractionMessage": "استخراج التحديث...",
+ "DialogUpdaterAlreadyOnLatestVersionMessage": "أنت تستخدم بالفعل أحدث إصدار من ريوجينكس!",
+ "DialogUpdaterFailedToGetVersionMessage": "حدث خطأ أثناء محاولة الحصول على معلومات الإصدار من إصدار غيت هاب. يمكن أن يحدث هذا إذا تم تجميع إصدار جديد بواسطة إجراءات غيت هاب. جرب مجددا بعد دقائق.",
+ "DialogUpdaterConvertFailedGithubMessage": "فشل تحويل إصدار ريوجينكس المستلم من إصدار غيت هاب.",
+ "DialogUpdaterDownloadingMessage": "جاري تنزيل التحديث...",
+ "DialogUpdaterExtractionMessage": "جاري استخراج التحديث...",
"DialogUpdaterRenamingMessage": "إعادة تسمية التحديث...",
"DialogUpdaterAddingFilesMessage": "إضافة تحديث جديد...",
"DialogUpdaterCompleteMessage": "اكتمل التحديث",
- "DialogUpdaterRestartMessage": "هل تريد إعادة تشغيل Ryujinx الآن؟",
+ "DialogUpdaterRestartMessage": "هل تريد إعادة تشغيل ريوجينكس الآن؟",
"DialogUpdaterNoInternetMessage": "أنت غير متصل بالإنترنت.",
"DialogUpdaterNoInternetSubMessage": "يرجى التحقق من أن لديك اتصال إنترنت فعال!",
- "DialogUpdaterDirtyBuildMessage": "لا يمكنك تحديث البناء القذر من Ryujinx!",
- "DialogUpdaterDirtyBuildSubMessage": "الرجاء تحميل Ryujinx على https://ryujinx.org/ إذا كنت تبحث عن إصدار مدعوم.",
+ "DialogUpdaterDirtyBuildMessage": "لا يمكنك تحديث نسخة القذرة من ريوجينكس!",
+ "DialogUpdaterDirtyBuildSubMessage": "الرجاء تحميل ريوجينكس من https://ryujinx.org إذا كنت تبحث عن إصدار مدعوم.",
"DialogRestartRequiredMessage": "يتطلب إعادة التشغيل",
"DialogThemeRestartMessage": "تم حفظ السمة. إعادة التشغيل مطلوبة لتطبيق السمة.",
"DialogThemeRestartSubMessage": "هل تريد إعادة التشغيل",
"DialogFirmwareInstallEmbeddedMessage": "هل ترغب في تثبيت البرنامج الثابت المدمج في هذه اللعبة؟ (البرنامج الثابت {0})",
- "DialogFirmwareInstallEmbeddedSuccessMessage": "لم يتم العثور على أي برنامج ثابت مثبت ولكن Ryujinx كان قادراً على تثبيت البرنامج الثابت {0} من اللعبة المقدمة.\nسيبدأ المحاكي الآن.",
+ "DialogFirmwareInstallEmbeddedSuccessMessage": "لم يتم العثور على أي برنامج ثابت مثبت ولكن ريوجينكس كان قادرا على تثبيت البرنامج الثابت {0} من اللعبة المقدمة.\nسيبدأ المحاكي الآن.",
"DialogFirmwareNoFirmwareInstalledMessage": "لا يوجد برنامج ثابت مثبت",
"DialogFirmwareInstalledMessage": "تم تثبيت البرنامج الثابت {0}",
"DialogInstallFileTypesSuccessMessage": "تم تثبيت أنواع الملفات بنجاح!",
@@ -349,38 +455,38 @@
"DialogUninstallFileTypesSuccessMessage": "تم إلغاء تثبيت أنواع الملفات بنجاح!",
"DialogUninstallFileTypesErrorMessage": "فشل إلغاء تثبيت أنواع الملفات.",
"DialogOpenSettingsWindowLabel": "فتح نافذة الإعدادات",
- "DialogControllerAppletTitle": "برنامج التحكم",
+ "DialogControllerAppletTitle": "تطبيق وحدة التحكم المصغر",
"DialogMessageDialogErrorExceptionMessage": "خطأ في عرض مربع حوار الرسالة: {0}",
"DialogSoftwareKeyboardErrorExceptionMessage": "خطأ في عرض لوحة مفاتيح البرامج: {0}",
- "DialogErrorAppletErrorExceptionMessage": "Error displaying ErrorApplet Dialog: {0}",
+ "DialogErrorAppletErrorExceptionMessage": "خطأ في عرض مربع حوار خطأ التطبيق المصغر: {0}",
"DialogUserErrorDialogMessage": "{0}: {1}",
"DialogUserErrorDialogInfoMessage": "لمزيد من المعلومات حول كيفية إصلاح هذا الخطأ، اتبع دليل الإعداد الخاص بنا.",
"DialogUserErrorDialogTitle": "خطأ ريوجينكس ({0})",
"DialogAmiiboApiTitle": "أميبو API",
- "DialogAmiiboApiFailFetchMessage": "حدث خطأ أثناء جلب المعلومات من واجهة برمجة التطبيقات.",
- "DialogAmiiboApiConnectErrorMessage": "غير قادر على الاتصال بخادم API Amiibo. قد تكون الخدمة متوقفة أو قد تحتاج إلى التحقق من اتصال الإنترنت الخاص بك على الإنترنت.",
+ "DialogAmiiboApiFailFetchMessage": "حدث خطأ أثناء جلب المعلومات من API.",
+ "DialogAmiiboApiConnectErrorMessage": "غير قادر على الاتصال بخادم API أميبو. قد تكون الخدمة معطلة أو قد تحتاج إلى التحقق من اتصالك بالإنترنت.",
"DialogProfileInvalidProfileErrorMessage": "الملف الشخصي {0} غير متوافق مع نظام تكوين الإدخال الحالي.",
"DialogProfileDefaultProfileOverwriteErrorMessage": "لا يمكن الكتابة فوق الملف الشخصي الافتراضي",
- "DialogProfileDeleteProfileTitle": "حذف ملف التعريف",
+ "DialogProfileDeleteProfileTitle": "حذف الملف الشخصي",
"DialogProfileDeleteProfileMessage": "هذا الإجراء لا رجعة فيه، هل أنت متأكد من أنك تريد المتابعة؟",
"DialogWarning": "تحذير",
- "DialogPPTCDeletionMessage": "أنت على وشك الإنتظار لإعادة بناء PTC على التمهيد التالي :\n\n{0}\n\nهل أنت متأكد من أنك تريد المتابعة؟",
- "DialogPPTCDeletionErrorMessage": "خطأ في إزالة ذاكرة التخزين المؤقت PPTC في {0}: {1}",
- "DialogShaderDeletionMessage": "أنت على وشك حذف ذاكرة التخزين المؤقت لـ Shader من أجل:\n\n{0}\n\nهل انت متأكد انك تريد المتابعة؟",
- "DialogShaderDeletionErrorMessage": "Error purging Shader cache at {0}: {1}",
- "DialogRyujinxErrorMessage": "واجه Ryujinx خطأ",
+ "DialogPPTCDeletionMessage": "أنت على وشك الإنتظار لإعادة بناء ذاكرة التخزين المؤقت للترجمة المستمرة (PPTC) عند الإقلاع التالي لـ:\n\n{0}\n\nأمتأكد من رغبتك في المتابعة؟",
+ "DialogPPTCDeletionErrorMessage": "خطأ خلال تنظيف ذاكرة التخزين المؤقت للترجمة المستمرة (PPTC) في {0}: {1}",
+ "DialogShaderDeletionMessage": "أنت على وشك حذف ذاكرة المظللات المؤقتة ل:\n\n{0}\n\nهل انت متأكد انك تريد المتابعة؟",
+ "DialogShaderDeletionErrorMessage": "حدث خطأ أثناء تنظيف ذاكرة المظللات المؤقتة في {0}: {1}",
+ "DialogRyujinxErrorMessage": "واجه ريوجينكس خطأ",
"DialogInvalidTitleIdErrorMessage": "خطأ في واجهة المستخدم: اللعبة المحددة لم يكن لديها معرف عنوان صالح",
- "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "لم يتم العثور على فريموير للنظام صالح في {0}.",
+ "DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "لم يتم العثور على برنامج ثابت للنظام صالح في {0}.",
"DialogFirmwareInstallerFirmwareInstallTitle": "تثبيت البرنامج الثابت {0}",
"DialogFirmwareInstallerFirmwareInstallMessage": "سيتم تثبيت إصدار النظام {0}.",
"DialogFirmwareInstallerFirmwareInstallSubMessage": "\n\nهذا سيحل محل إصدار النظام الحالي {0}.",
"DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\nهل تريد المتابعة؟",
"DialogFirmwareInstallerFirmwareInstallWaitMessage": "تثبيت البرنامج الثابت...",
"DialogFirmwareInstallerFirmwareInstallSuccessMessage": "تم تثبيت إصدار النظام {0} بنجاح.",
- "DialogUserProfileDeletionWarningMessage": "لن تكون هناك ملفات تعريف أخرى لفتحها إذا تم حذف الملف الشخصي المحدد",
+ "DialogUserProfileDeletionWarningMessage": "لن تكون هناك ملفات الشخصية أخرى لفتحها إذا تم حذف الملف الشخصي المحدد",
"DialogUserProfileDeletionConfirmMessage": "هل تريد حذف الملف الشخصي المحدد",
- "DialogUserProfileUnsavedChangesTitle": "تحذير - التغييرات غير المحفوظة",
- "DialogUserProfileUnsavedChangesMessage": "لقد قمت بإجراء تغييرات على ملف تعريف المستخدم هذا ولم يتم حفظها.",
+ "DialogUserProfileUnsavedChangesTitle": "تحذير - التغييرات غير محفوظة",
+ "DialogUserProfileUnsavedChangesMessage": "لقد قمت بإجراء تغييرات على الملف الشخصي لهذا المستخدم هذا ولم يتم حفظها.",
"DialogUserProfileUnsavedChangesSubMessage": "هل تريد تجاهل التغييرات؟",
"DialogControllerSettingsModifiedConfirmMessage": "تم تحديث إعدادات وحدة التحكم الحالية.",
"DialogControllerSettingsModifiedConfirmSubMessage": "هل تريد الحفظ ؟",
@@ -388,29 +494,29 @@
"DialogModAlreadyExistsMessage": "التعديل موجود بالفعل",
"DialogModInvalidMessage": "المجلد المحدد لا يحتوي على تعديل!",
"DialogModDeleteNoParentMessage": "فشل الحذف: لم يمكن العثور على المجلد الرئيسي للتعديل\"{0}\"!",
- "DialogDlcNoDlcErrorMessage": "الملف المحدد لا يحتوي على DLC للعنوان المحدد!",
+ "DialogDlcNoDlcErrorMessage": "الملف المحدد لا يحتوي على محتوى إضافي للعنوان المحدد!",
"DialogPerformanceCheckLoggingEnabledMessage": "لقد تم تمكين تسجيل التتبع، والذي تم تصميمه ليتم استخدامه من قبل المطورين فقط.",
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "للحصول على الأداء الأمثل، يوصى بتعطيل تسجيل التتبع. هل ترغب في تعطيل تسجيل التتبع الآن؟",
- "DialogPerformanceCheckShaderDumpEnabledMessage": "لقد قمت بتمكين تفريغ التظليل، والذي تم تصميمه ليستخدمه المطورون فقط.",
- "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "For optimal performance, it's recommended to disable shader dumping. Would you like to disable shader dumping now?",
+ "DialogPerformanceCheckShaderDumpEnabledMessage": "لقد قمت بتمكين تفريغ المظللات، والذي تم تصميمه ليستخدمه المطورون فقط.",
+ "DialogPerformanceCheckShaderDumpEnabledConfirmMessage": "للحصول على الأداء الأمثل، يوصى بتعطيل تفريغ المظللات. هل ترغب في تعطيل تفريغ المظللات الآن؟",
"DialogLoadAppGameAlreadyLoadedMessage": "تم تحميل لعبة بالفعل",
"DialogLoadAppGameAlreadyLoadedSubMessage": "الرجاء إيقاف المحاكاة أو إغلاق المحاكي قبل بدء لعبة أخرى.",
- "DialogUpdateAddUpdateErrorMessage": "الملف المحدد لا يحتوي على تحديث للملف المحدد!",
- "DialogSettingsBackendThreadingWarningTitle": "Warning - Backend Threading",
- "DialogSettingsBackendThreadingWarningMessage": "Ryujinx must be restarted after changing this option for it to apply fully. Depending on your platform, you may need to manually disable your driver's own multithreading when using Ryujinx's.",
+ "DialogUpdateAddUpdateErrorMessage": "الملف المحدد لا يحتوي على تحديث للعنوان المحدد!",
+ "DialogSettingsBackendThreadingWarningTitle": "تحذير - خلفية متعددة المسارات",
+ "DialogSettingsBackendThreadingWarningMessage": "يجب إعادة تشغيل ريوجينكس بعد تغيير هذا الخيار حتى يتم تطبيقه بالكامل. اعتمادا على النظام الأساسي الخاص بك، قد تحتاج إلى تعطيل تعدد المسارات الخاص ببرنامج الرسومات التشغيل الخاص بك يدويًا عند استخدام الخاص بريوجينكس.",
"DialogModManagerDeletionWarningMessage": "أنت على وشك حذف التعديل: {0}\n\nهل انت متأكد انك تريد المتابعة؟",
"DialogModManagerDeletionAllWarningMessage": "أنت على وشك حذف كافة التعديلات لهذا العنوان.\n\nهل انت متأكد انك تريد المتابعة؟",
"SettingsTabGraphicsFeaturesOptions": "المميزات",
- "SettingsTabGraphicsBackendMultithreading": "Graphics Backend Multithreading:",
+ "SettingsTabGraphicsBackendMultithreading": "تعدد المسارات لخلفية الرسومات:",
"CommonAuto": "تلقائي",
- "CommonOff": "إيقاف",
+ "CommonOff": "معطل",
"CommonOn": "تشغيل",
"InputDialogYes": "نعم",
"InputDialogNo": "لا",
"DialogProfileInvalidProfileNameErrorMessage": "يحتوي اسم الملف على أحرف غير صالحة. يرجى المحاولة مرة أخرى.",
"MenuBarOptionsPauseEmulation": "إيقاف مؤقت",
"MenuBarOptionsResumeEmulation": "استئناف",
- "AboutUrlTooltipMessage": "انقر لفتح موقع Ryujinx في متصفحك الافتراضي.",
+ "AboutUrlTooltipMessage": "انقر لفتح موقع ريوجينكس في متصفحك الافتراضي.",
"AboutDisclaimerMessage": "ريوجينكس لا ينتمي إلى نينتندو™،\nأو أي من شركائها بأي شكل من الأشكال.",
"AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) يتم \nاستخدامه في محاكاة أمبيو لدينا.",
"AboutPatreonUrlTooltipMessage": "انقر لفتح صفحة ريوجينكس في باتريون في متصفحك الافتراضي.",
@@ -419,18 +525,18 @@
"AboutTwitterUrlTooltipMessage": "انقر لفتح صفحة ريوجينكس في تويتر في متصفحك الافتراضي.",
"AboutRyujinxAboutTitle": "حول:",
"AboutRyujinxAboutContent": "ريوجينكس هو محاكي لجهاز نينتندو سويتش™.\nمن فضلك ادعمنا على باتريون.\nاحصل على آخر الأخبار على تويتر أو ديسكورد.\nيمكن للمطورين المهتمين بالمساهمة معرفة المزيد على غيت هاب أو ديسكورد.",
- "AboutRyujinxMaintainersTitle": "تم إصلاحها بواسطة:",
+ "AboutRyujinxMaintainersTitle": "تتم صيانته بواسطة:",
"AboutRyujinxMaintainersContentTooltipMessage": "انقر لفتح صفحة المساهمين في متصفحك الافتراضي.",
"AboutRyujinxSupprtersTitle": "مدعوم على باتريون بواسطة:",
"AmiiboSeriesLabel": "مجموعة أميبو",
"AmiiboCharacterLabel": "شخصية",
"AmiiboScanButtonLabel": "فحصه",
"AmiiboOptionsShowAllLabel": "إظهار كل أميبو",
- "AmiiboOptionsUsRandomTagLabel": "Hack: Use Random tag Uuid",
+ "AmiiboOptionsUsRandomTagLabel": "هاك: استخدم علامة Uuid عشوائية ",
"DlcManagerTableHeadingEnabledLabel": "مفعل",
"DlcManagerTableHeadingTitleIdLabel": "معرف العنوان",
"DlcManagerTableHeadingContainerPathLabel": "مسار الحاوية",
- "DlcManagerTableHeadingFullPathLabel": "المسار كاملاً",
+ "DlcManagerTableHeadingFullPathLabel": "المسار كاملا",
"DlcManagerRemoveAllButton": "حذف الكل",
"DlcManagerEnableAllButton": "تشغيل الكل",
"DlcManagerDisableAllButton": "تعطيل الكل",
@@ -440,100 +546,100 @@
"CommonSort": "فرز",
"CommonShowNames": "عرض الأسماء",
"CommonFavorite": "المفضلة",
- "OrderAscending": "ترتيب تصاعدي",
- "OrderDescending": "ترتيب تنازلي",
+ "OrderAscending": "تصاعدي",
+ "OrderDescending": "تنازلي",
"SettingsTabGraphicsFeatures": "الميزات والتحسينات",
"ErrorWindowTitle": "نافذة الخطأ",
- "ToggleDiscordTooltip": "اختر ما إذا كنت تريد عرض ريوجينكس في نشاط ديسكورد \"يتم تشغيله حاليًا\" أم لا",
- "AddGameDirBoxTooltip": "أدخل دليل اللعبة لإضافته إلى القائمة",
- "AddGameDirTooltip": "إضافة دليل اللعبة إلى القائمة",
- "RemoveGameDirTooltip": "إزالة دليل اللعبة المحدد",
+ "ToggleDiscordTooltip": "اختر ما إذا كنت تريد عرض ريوجينكس في نشاط ديسكورد \"يتم تشغيله حاليا\" أم لا",
+ "AddGameDirBoxTooltip": "أدخل مجلد اللعبة لإضافته إلى القائمة",
+ "AddGameDirTooltip": "إضافة مجلد اللعبة إلى القائمة",
+ "RemoveGameDirTooltip": "إزالة مجلد اللعبة المحدد",
"CustomThemeCheckTooltip": "استخدم سمة أفالونيا المخصصة لواجهة المستخدم الرسومية لتغيير مظهر قوائم المحاكي",
"CustomThemePathTooltip": "مسار سمة واجهة المستخدم المخصصة",
"CustomThemeBrowseTooltip": "تصفح للحصول على سمة واجهة المستخدم المخصصة",
- "DockModeToggleTooltip": "يجعل وضع الإرساء النظام الذي تمت محاكاته بمثابة جهاز نينتندو سويتش الذي تم إرساءه. يؤدي هذا إلى تحسين الدقة الرسومية في معظم الألعاب. على العكس من ذلك، سيؤدي تعطيل هذا إلى جعل النظام الذي تمت محاكاته يعمل كجهاز نينتندو سويتش محمول، مما يقلل من جودة الرسومات.\n\nقم بتكوين عناصر تحكم اللاعب 1 إذا كنت تخطط لاستخدام وضع الإرساء؛ قم بتكوين عناصر التحكم المحمولة إذا كنت تخطط لاستخدام الوضع المحمول.\n\nاتركه مشغل إذا لم تكن متأكدًا.",
- "DirectKeyboardTooltip": "دعم الوصول المباشر إلى لوحة المفاتيح (HID). يوفر وصول الألعاب إلى لوحة المفاتيح الخاصة بك كجهاز لإدخال النص.\n\nيعمل فقط مع الألعاب التي تدعم استخدام لوحة المفاتيح في الأصل على أجهزة سويتش.\n\nاتركه معطلا إذا كنت غير متأكد.",
- "DirectMouseTooltip": "دعم الوصول المباشر إلى لوحة المفاتيح (HID). يوفر وصول الألعاب إلى لوحة المفاتيح الخاصة بك كجهاز لإدخال النص.\n\nيعمل فقط مع الألعاب التي تدعم استخدام لوحة المفاتيح في الأصل على أجهزة سويتش.\n\nاتركه معطلا إذا كنت غير متأكد.",
+ "DockModeToggleTooltip": "يجعل وضع تركيب بالمنصة النظام الذي تمت محاكاته بمثابة جهاز نينتندو سويتش الذي تم تركيبه بالمنصة. يؤدي هذا إلى تحسين الدقة الرسومية في معظم الألعاب. على العكس من ذلك، سيؤدي تعطيل هذا إلى جعل النظام الذي تمت محاكاته يعمل كجهاز نينتندو سويتش محمول، مما يقلل من جودة الرسومات.\n\nقم بتكوين عناصر تحكم اللاعب 1 إذا كنت تخطط لاستخدام وضع تركيب بالمنصة؛ قم بتكوين عناصر التحكم المحمولة إذا كنت تخطط لاستخدام الوضع المحمول.\n\nاتركه مشغل إذا لم تكن متأكدا.",
+ "DirectKeyboardTooltip": "دعم الوصول المباشر للوحة المفاتيح (HID). يوفر وصول الألعاب إلى لوحة المفاتيح الخاصة بك كجهاز لإدخال النص.\n\nيعمل فقط مع الألعاب التي تدعم استخدام لوحة المفاتيح في الأصل على أجهزة سويتش.\n\nاتركه معطلا إذا كنت غير متأكد.",
+ "DirectMouseTooltip": "دعم الوصول المباشر للوحة المفاتيح (HID). يوفر وصول الألعاب إلى لوحة المفاتيح الخاصة بك كجهاز لإدخال النص.\n\nيعمل فقط مع الألعاب التي تدعم استخدام لوحة المفاتيح في الأصل على أجهزة سويتش.\n\nاتركه معطلا إذا كنت غير متأكد.",
"RegionTooltip": "تغيير منطقة النظام",
"LanguageTooltip": "تغيير لغة النظام",
- "TimezoneTooltip": "تغيير المنطقة الزمنية للنظام",
+ "TimezoneTooltip": "تغيير النطاق الزمني للنظام",
"TimeTooltip": "تغيير وقت النظام",
- "VSyncToggleTooltip": "Emulated console's Vertical Sync. Essentially a frame-limiter for the majority of games; disabling it may cause games to run at higher speed or make loading screens take longer or get stuck.\n\nCan be toggled in-game with a hotkey of your preference (F1 by default). We recommend doing this if you plan on disabling it.\n\nLeave ON if unsure.",
- "PptcToggleTooltip": "Saves translated JIT functions so that they do not need to be translated every time the game loads.\n\nReduces stuttering and significantly speeds up boot times after the first boot of a game.\n\nLeave ON if unsure.",
+ "VSyncToggleTooltip": "محاكاة المزامنة العمودية للجهاز. في الأساس محدد الإطار لغالبية الألعاب؛ قد يؤدي تعطيله إلى تشغيل الألعاب بسرعة أعلى أو جعل شاشات التحميل تستغرق وقتا أطول أو تتعطل.\n\nيمكن تبديله داخل اللعبة باستخدام مفتاح التشغيل السريع الذي تفضله (F1 افتراضيا). نوصي بالقيام بذلك إذا كنت تخطط لتعطيله.\n\nاتركه ممكنا إذا لم تكن متأكدا.",
+ "PptcToggleTooltip": "يحفظ وظائف JIT المترجمة بحيث لا تحتاج إلى ترجمتها في كل مرة يتم فيها تحميل اللعبة.\n\nيقلل من التقطيع ويسرع بشكل ملحوظ أوقات التشغيل بعد التشغيل الأول للعبة.\n\nاتركه ممكنا إذا لم تكن متأكدا.",
"FsIntegrityToggleTooltip": "يتحقق من وجود ملفات تالفة عند تشغيل لعبة ما، وإذا تم اكتشاف ملفات تالفة، فسيتم عرض خطأ تجزئة في السجل.\n\nليس له أي تأثير على الأداء ويهدف إلى المساعدة في استكشاف الأخطاء وإصلاحها.\n\nاتركه مفعلا إذا كنت غير متأكد.",
- "AudioBackendTooltip": "Changes the backend used to render audio.\n\nSDL2 is the preferred one, while OpenAL and SoundIO are used as fallbacks. Dummy will have no sound.\n\nSet to SDL2 if unsure.",
- "MemoryManagerTooltip": "Change how guest memory is mapped and accessed. Greatly affects emulated CPU performance.\n\nSet to HOST UNCHECKED if unsure.",
+ "AudioBackendTooltip": "يغير الواجهة الخلفية المستخدمة لتقديم الصوت.\n\nSDL2 هو الخيار المفضل، بينما يتم استخدام OpenAL وSoundIO كبديلين. زائف لن يكون لها صوت.\n\nاضبط على SDL2 إذا لم تكن متأكدا.",
+ "MemoryManagerTooltip": "تغيير كيفية تعيين ذاكرة الضيف والوصول إليها. يؤثر بشكل كبير على أداء وحدة المعالجة المركزية التي تمت محاكاتها.\n\nاضبط على المضيف غير محدد إذا لم تكن متأكدا.",
"MemoryManagerSoftwareTooltip": "استخدام جدول الصفحات البرمجي لترجمة العناوين. أعلى دقة ولكن أبطأ أداء.",
- "MemoryManagerHostTooltip": "Directly map memory in the host address space. Much faster JIT compilation and execution.",
- "MemoryManagerUnsafeTooltip": "Directly map memory, but do not mask the address within the guest address space before access. Faster, but at the cost of safety. The guest application can access memory from anywhere in Ryujinx, so only run programs you trust with this mode.",
- "UseHypervisorTooltip": "Use Hypervisor instead of JIT. Greatly improves performance when available, but can be unstable in its current state.",
- "DRamTooltip": "Utilizes an alternative MemoryMode layout to mimic a Switch development model.\n\nThis is only useful for higher-resolution texture packs or 4k resolution mods. Does NOT improve performance.\n\nLeave OFF if unsure.",
+ "MemoryManagerHostTooltip": "تعيين الذاكرة مباشرة في مساحة عنوان المضيف. تجميع وتنفيذ JIT أسرع بكثير.",
+ "MemoryManagerUnsafeTooltip": "تعيين الذاكرة مباشرة، ولكن لا تخفي العنوان داخل مساحة عنوان الضيف قبل الوصول. أسرع، ولكن على حساب السلامة. يمكن لتطبيق الضيف الوصول إلى الذاكرة من أي مكان في ريوجينكس، لذا قم بتشغيل البرامج التي تثق بها فقط مع هذا الوضع.",
+ "UseHypervisorTooltip": "استخدم هايبرڤايزور بدلا من JIT. يعمل على تحسين الأداء بشكل كبير عند توفره، ولكنه قد يكون غير مستقر في حالته الحالية.",
+ "DRamTooltip": "يستخدم تخطيط وضع الذاكرة البديل لتقليد نموذج سويتش المطورين.\n\nيعد هذا مفيدا فقط لحزم النسيج عالية الدقة أو تعديلات دقة 4K. لا يحسن الأداء.\n\nاتركه معطلا إذا لم تكن متأكدا.",
"IgnoreMissingServicesTooltip": "يتجاهل خدمات نظام هوريزون غير المنفذة. قد يساعد هذا في تجاوز الأعطال عند تشغيل ألعاب معينة.\n\nاتركه معطلا إذا كنت غير متأكد.",
- "GraphicsBackendThreadingTooltip": "Executes graphics backend commands on a second thread.\n\nSpeeds up shader compilation, reduces stuttering, and improves performance on GPU drivers without multithreading support of their own. Slightly better performance on drivers with multithreading.\n\nSet to AUTO if unsure.",
- "GalThreadingTooltip": "Executes graphics backend commands on a second thread.\n\nSpeeds up shader compilation, reduces stuttering, and improves performance on GPU drivers without multithreading support of their own. Slightly better performance on drivers with multithreading.\n\nSet to AUTO if unsure.",
- "ShaderCacheToggleTooltip": "Saves a disk shader cache which reduces stuttering in subsequent runs.\n\nLeave ON if unsure.",
- "ResolutionScaleTooltip": "يضاعف دقة عرض اللعبة.\n\nقد لا تعمل بعض الألعاب مع هذا وتبدو منقطة حتى عند زيادة الدقة؛ بالنسبة لهذه الألعاب، قد تحتاج إلى العثور على تعديلات تزيل الصقل أو تزيد من دقة العرض الداخلي. لاستخدام الأخير، من المحتمل أن ترغب في تحديد أصلي.\n\nيمكن تغيير هذا الخيار أثناء تشغيل اللعبة بالنقر فوق \"تطبيق\" أدناه؛ يمكنك ببساطة تحريك نافذة الإعدادات جانبًا والتجربة حتى تجد المظهر المفضل للعبة.\n\nضع في اعتبارك أن 4x مبالغة في أي إعداد تقريبًا.",
+ "GraphicsBackendThreadingTooltip": "ينفذ أوامر الواجهة الخلفية للرسومات على مسار ثاني.\n\nيعمل على تسريع عملية تجميع المظللات وتقليل التقطيع وتحسين الأداء على برامج تشغيل وحدة الرسوميات دون دعم المسارات المتعددة الخاصة بهم. أداء أفضل قليلا على برامج التشغيل ذات المسارات المتعددة.\n\nاضبط على تلقائي إذا لم تكن متأكدا.",
+ "GalThreadingTooltip": "ينفذ أوامر الواجهة الخلفية للرسومات على مسار ثاني.\n\nيعمل على تسريع عملية تجميع المظللات وتقليل التقطيع وتحسين الأداء على برامج تشغيل وحدة الرسوميات دون دعم المسارات المتعددة الخاصة بهم. أداء أفضل قليلا على برامج التشغيل ذات المسارات المتعددة.\n\nاضبط على تلقائي إذا لم تكن متأكدا.",
+ "ShaderCacheToggleTooltip": "يحفظ ذاكرة المظللات المؤقتة على القرص مما يقلل من التقطيع في عمليات التشغيل اللاحقة.\n\nاتركه مفعلا إذا لم تكن متأكدا.",
+ "ResolutionScaleTooltip": "يضاعف دقة عرض اللعبة.\n\nقد لا تعمل بعض الألعاب مع هذا وتبدو منقطة حتى عند زيادة الدقة؛ بالنسبة لهذه الألعاب، قد تحتاج إلى العثور على تعديلات تزيل تنعيم الحواف أو تزيد من دقة العرض الداخلي. لاستخدام الأخير، من المحتمل أن ترغب في تحديد أصلي.\n\nيمكن تغيير هذا الخيار أثناء تشغيل اللعبة بالنقر فوق \"تطبيق\" أدناه؛ يمكنك ببساطة تحريك نافذة الإعدادات جانبًا والتجربة حتى تجد المظهر المفضل للعبة.\n\nضع في اعتبارك أن 4x مبالغة في أي إعداد تقريبًا.",
"ResolutionScaleEntryTooltip": "مقياس دقة النقطة العائمة، مثل 1.5. من المرجح أن تتسبب المقاييس غير المتكاملة في حدوث مشكلات أو تعطل.",
- "AnisotropyTooltip": "مستوى تصفية متباين الخواص. اضبط على تلقائي لاستخدام القيمة التي تطلبها اللعبة.",
- "AspectRatioTooltip": "يتم تطبيق نسبة العرض إلى الارتفاع على نافذة العارض.\n\nقم بتغيير هذا فقط إذا كنت تستخدم تعديل نسبة العرض إلى الارتفاع للعبتك، وإلا سيتم تمديد الرسومات.\n\nاتركه على 16:9 إذا لم تكن متأكدًا.",
- "ShaderDumpPathTooltip": "Graphics Shaders Dump Path",
+ "AnisotropyTooltip": "مستوى تصفية. اضبط على تلقائي لاستخدام القيمة التي تطلبها اللعبة.",
+ "AspectRatioTooltip": "يتم تطبيق نسبة العرض إلى الارتفاع على نافذة العارض.\n\nقم بتغيير هذا فقط إذا كنت تستخدم تعديل نسبة العرض إلى الارتفاع للعبتك، وإلا سيتم تمديد الرسومات.\n\nاتركه16:9 إذا لم تكن متأكدا.",
+ "ShaderDumpPathTooltip": "مسار تفريغ المظللات",
"FileLogTooltip": "حفظ تسجيل وحدة التحكم إلى ملف سجل على القرص. لا يؤثر على الأداء.",
- "StubLogTooltip": "Prints stub log messages in the console. Does not affect performance.",
- "InfoLogTooltip": "Prints info log messages in the console. Does not affect performance.",
+ "StubLogTooltip": "طباعة رسائل سجل stub في وحدة التحكم. لا يؤثر على الأداء.",
+ "InfoLogTooltip": "طباعة رسائل سجل المعلومات في وحدة التحكم. لا يؤثر على الأداء.",
"WarnLogTooltip": "طباعة رسائل سجل التحذير في وحدة التحكم. لا يؤثر على الأداء.",
"ErrorLogTooltip": "طباعة رسائل سجل الأخطاء في وحدة التحكم. لا يؤثر على الأداء.",
"TraceLogTooltip": "طباعة رسائل سجل التتبع في وحدة التحكم. لا يؤثر على الأداء.",
"GuestLogTooltip": "طباعة رسائل سجل الضيف في وحدة التحكم. لا يؤثر على الأداء.",
"FileAccessLogTooltip": "طباعة رسائل سجل الوصول إلى الملفات في وحدة التحكم.",
- "FSAccessLogModeTooltip": "Enables FS access log output to the console. Possible modes are 0-3",
+ "FSAccessLogModeTooltip": "تمكين إخراج سجل الوصول إلى نظام الملفات إلى وحدة التحكم. الأوضاع الممكنة هي 0-3",
"DeveloperOptionTooltip": "استخدمه بعناية",
"OpenGlLogLevel": "يتطلب تمكين مستويات السجل المناسبة",
"DebugLogTooltip": "طباعة رسائل سجل التصحيح في وحدة التحكم.\n\nاستخدم هذا فقط إذا طلب منك أحد الموظفين تحديدًا ذلك، لأنه سيجعل من الصعب قراءة السجلات وسيؤدي إلى تدهور أداء المحاكي.",
- "LoadApplicationFileTooltip": "افتح مستكشف الملفات لاختيار ملف متوافق مع Switch لتحميله",
- "LoadApplicationFolderTooltip": "افتح مستكشف الملفات لاختيار تطبيق متوافق مع Switch للتحميل",
- "OpenRyujinxFolderTooltip": "فتح مجلد نظام ملفات Ryujinx",
+ "LoadApplicationFileTooltip": "افتح مستكشف الملفات لاختيار ملف متوافق مع سويتش لتحميله",
+ "LoadApplicationFolderTooltip": "افتح مستكشف الملفات لاختيار تطبيق متوافق مع سويتش للتحميل",
+ "OpenRyujinxFolderTooltip": "فتح مجلد نظام ملفات ريوجينكس",
"OpenRyujinxLogsTooltip": "يفتح المجلد الذي تتم كتابة السجلات إليه",
- "ExitTooltip": "الخروج من Ryujinx",
+ "ExitTooltip": "الخروج من ريوجينكس",
"OpenSettingsTooltip": "فتح نافذة الإعدادات",
- "OpenProfileManagerTooltip": "فتح نافذة إدارة ملفات تعريف المستخدمين",
+ "OpenProfileManagerTooltip": "فتح نافذة إدارة الملفات الشخصية للمستخدمين",
"StopEmulationTooltip": "إيقاف محاكاة اللعبة الحالية والعودة إلى اختيار اللعبة",
- "CheckUpdatesTooltip": "التحقق من وجود تحديثات لـ Ryujinx",
+ "CheckUpdatesTooltip": "التحقق من وجود تحديثات لريوجينكس",
"OpenAboutTooltip": "فتح حول النافذة",
"GridSize": "حجم الشبكة",
"GridSizeTooltip": "تغيير حجم عناصر الشبكة",
"SettingsTabSystemSystemLanguageBrazilianPortuguese": "البرتغالية البرازيلية",
"AboutRyujinxContributorsButtonHeader": "رؤية جميع المساهمين",
- "SettingsTabSystemAudioVolume": "الحجم:",
+ "SettingsTabSystemAudioVolume": "مستوى الصوت:",
"AudioVolumeTooltip": "تغيير مستوى الصوت",
"SettingsTabSystemEnableInternetAccess": "الوصول إلى إنترنت كضيف/وضع LAN",
- "EnableInternetAccessTooltip": "للسماح للتطبيق المحاكي بالاتصال بالإنترنت.\n\nالألعاب ذات وضع الشبكة المحلية يمكن الاتصال ببعضها البعض عندما يتم تمكين هذا النظام وتكون الأنظمة متصلة بنفس نقطة الوصول. هذا يشمل وحدات التحكم الحقيقية أيضًا.\n\nلا يسمح بالاتصال بخوادم Nintendo. قد يسبب الانهيار في بعض الألعاب التي تحاول الاتصال بالإنترنت.\n\nاتركه متوقفا إن لم يكن مؤكدا.",
+ "EnableInternetAccessTooltip": "للسماح للتطبيق الذي تمت محاكاته بالاتصال بالإنترنت.\n\nيمكن للألعاب التي تحتوي على وضع LAN الاتصال ببعضها البعض عند تمكين ذلك وتوصيل الأنظمة بنفس نقطة الوصول. وهذا يشمل الأجهزة الحقيقية أيضا.\n\nلا يسمح بالاتصال بخوادم نينتندو. قد يتسبب في حدوث عطل في بعض الألعاب التي تحاول الاتصال بالإنترنت.\n\nاتركه معطلا إذا لم تكن متأكدا.",
"GameListContextMenuManageCheatToolTip": "إدارة الغش",
"GameListContextMenuManageCheat": "إدارة الغش",
"GameListContextMenuManageModToolTip": "إدارة التعديلات",
"GameListContextMenuManageMod": "إدارة التعديلات",
"ControllerSettingsStickRange": "نطاق:",
- "DialogStopEmulationTitle": "Ryujinx - إيقاف المحاكاة",
+ "DialogStopEmulationTitle": "ريوجينكس - إيقاف المحاكاة",
"DialogStopEmulationMessage": "هل أنت متأكد أنك تريد إيقاف المحاكاة؟",
- "SettingsTabCpu": "CPU",
+ "SettingsTabCpu": "المعالج",
"SettingsTabAudio": "الصوت",
"SettingsTabNetwork": "الشبكة",
"SettingsTabNetworkConnection": "اتصال الشبكة",
"SettingsTabCpuCache": "ذاكرة المعالج المؤقت",
"SettingsTabCpuMemory": "وضع المعالج",
- "DialogUpdaterFlatpakNotSupportedMessage": "الرجاء تحديث Ryujinx عبر FlatHub.",
- "UpdaterDisabledWarningTitle": "التحديث معطل!",
+ "DialogUpdaterFlatpakNotSupportedMessage": "الرجاء تحديث ريوجينكس عبر فلات هاب.",
+ "UpdaterDisabledWarningTitle": "المحدث معطل!",
"ControllerSettingsRotate90": "تدوير 90 درجة في اتجاه عقارب الساعة",
"IconSize": "حجم الأيقونة",
"IconSizeTooltip": "تغيير حجم أيقونات اللعبة",
"MenuBarOptionsShowConsole": "عرض وحدة التحكم",
- "ShaderCachePurgeError": "Error purging shader cache at {0}: {1}",
+ "ShaderCachePurgeError": "حدث خطأ أثناء تنظيف ذاكرة المظللات المؤقتة في {0}: {1}",
"UserErrorNoKeys": "المفاتيح غير موجودة",
"UserErrorNoFirmware": "لم يتم العثور على البرنامج الثابت",
"UserErrorFirmwareParsingFailed": "خطأ في تحليل البرنامج الثابت",
"UserErrorApplicationNotFound": "التطبيق غير موجود",
"UserErrorUnknown": "خطأ غير معروف",
"UserErrorUndefined": "خطأ غير محدد",
- "UserErrorNoKeysDescription": "لم يتمكن Ryujinx من العثور على ملف 'prod.keys' الخاص بك",
+ "UserErrorNoKeysDescription": "لم يتمكن ريوجينكس من العثور على ملف 'prod.keys' الخاص بك",
"UserErrorNoFirmwareDescription": "لم يتمكن ريوجينكس من العثور على أية برامج ثابتة مثبتة",
"UserErrorFirmwareParsingFailedDescription": "لم يتمكن ريوجينكس من تحليل البرامج الثابتة المتوفرة. يحدث هذا عادة بسبب المفاتيح القديمة.",
"UserErrorApplicationNotFoundDescription": "تعذر على ريوجينكس العثور على تطبيق صالح في المسار المحدد.",
@@ -542,72 +648,73 @@
"OpenSetupGuideMessage": "فتح دليل الإعداد",
"NoUpdate": "لا يوجد تحديث",
"TitleUpdateVersionLabel": "الإصدار: {0}",
- "RyujinxInfo": "Ryujinx - معلومات",
- "RyujinxConfirm": "Ryujinx - تأكيد",
+ "RyujinxInfo": "ريوجينكس - معلومات",
+ "RyujinxConfirm": "ريوجينكس - تأكيد",
"FileDialogAllTypes": "كل الأنواع",
- "Never": "مطلقاً",
- "SwkbdMinCharacters": "يجب أن يبلغ طوله {0} حرفًا على الأقل",
- "SwkbdMinRangeCharacters": "يجب أن يتكون من {0}-{1} حرفًا",
+ "Never": "مطلقا",
+ "SwkbdMinCharacters": "يجب أن يبلغ طوله {0} حرفا على الأقل",
+ "SwkbdMinRangeCharacters": "يجب أن يتكون من {0}-{1} حرفا",
"SoftwareKeyboard": "لوحة المفاتيح البرمجية",
"SoftwareKeyboardModeNumeric": "يجب أن يكون 0-9 أو '.' فقط",
"SoftwareKeyboardModeAlphabet": "يجب أن تكون الأحرف غير CJK فقط",
"SoftwareKeyboardModeASCII": "يجب أن يكون نص ASCII فقط",
- "ControllerAppletControllers": "ذراع التحكم المدعومة:",
+ "ControllerAppletControllers": "وحدات التحكم المدعومة:",
"ControllerAppletPlayers": "اللاعبين:",
"ControllerAppletDescription": "الإعدادات الحالية غير صالحة. افتح الإعدادات وأعد تكوين المدخلات الخاصة بك.",
- "ControllerAppletDocked": "Docked mode set. Handheld control should be disabled.",
+ "ControllerAppletDocked": "تم ضبط وضع تركيب بالمنصة. يجب تعطيل التحكم المحمول.",
"UpdaterRenaming": "إعادة تسمية الملفات القديمة...",
- "UpdaterRenameFailed": "التحديث غير قادر على إعادة تسمية الملف: {0}",
+ "UpdaterRenameFailed": "المحدث غير قادر على إعادة تسمية الملف: {0}",
"UpdaterAddingFiles": "إضافة ملفات جديدة...",
"UpdaterExtracting": "استخراج التحديث...",
"UpdaterDownloading": "تحميل التحديث...",
"Game": "لعبة",
- "Docked": "مركب بالمنصة",
+ "Docked": "تركيب بالمنصة",
"Handheld": "محمول",
"ConnectionError": "خطأ في الاتصال",
"AboutPageDeveloperListMore": "{0} والمزيد...",
"ApiError": "خطأ في API.",
- "LoadingHeading": "جارٍ تحميل {0}",
- "CompilingPPTC": "تجميع الـ PTC",
- "CompilingShaders": "تجميع الظلال",
+ "LoadingHeading": "جاري تحميل {0}",
+ "CompilingPPTC": "تجميع الـ(PPTC)",
+ "CompilingShaders": "تجميع المظللات",
"AllKeyboards": "كل لوحات المفاتيح",
"OpenFileDialogTitle": "حدد ملف مدعوم لفتحه",
- "OpenFolderDialogTitle": "حدد مجلدًا يحتوي على لعبة غير مضغوطة",
+ "OpenFolderDialogTitle": "حدد مجلدا يحتوي على لعبة غير مضغوطة",
"AllSupportedFormats": "كل التنسيقات المدعومة",
- "RyujinxUpdater": "تحديث Ryujinx",
+ "RyujinxUpdater": "محدث ريوجينكس",
"SettingsTabHotkeys": "مفاتيح الاختصار في لوحة المفاتيح",
"SettingsTabHotkeysHotkeys": "مفاتيح الاختصار في لوحة المفاتيح",
- "SettingsTabHotkeysToggleVsyncHotkey": "تبديل VSync:",
+ "SettingsTabHotkeysToggleVsyncHotkey": "تبديل المزامنة العمودية:",
"SettingsTabHotkeysScreenshotHotkey": "لقطة الشاشة:",
"SettingsTabHotkeysShowUiHotkey": "عرض واجهة المستخدم:",
"SettingsTabHotkeysPauseHotkey": "إيقاف مؤقت:",
- "SettingsTabHotkeysToggleMuteHotkey": "كتم الصوت:",
+ "SettingsTabHotkeysToggleMuteHotkey": "كتم:",
"ControllerMotionTitle": "إعدادات التحكم بالحركة",
"ControllerRumbleTitle": "إعدادات الهزاز",
"SettingsSelectThemeFileDialogTitle": "حدد ملف السمة",
- "SettingsXamlThemeFile": "Xaml Theme File",
+ "SettingsXamlThemeFile": "ملف سمة Xaml",
"AvatarWindowTitle": "إدارة الحسابات - الصورة الرمزية",
"Amiibo": "أميبو",
"Unknown": "غير معروف",
"Usage": "الاستخدام",
"Writable": "قابل للكتابة",
- "SelectDlcDialogTitle": "حدد ملفات DLC",
+ "SelectDlcDialogTitle": "حدد ملفات المحتوي الإضافي",
"SelectUpdateDialogTitle": "حدد ملفات التحديث",
"SelectModDialogTitle": "حدد مجلد التعديل",
- "UserProfileWindowTitle": "مدير ملفات تعريف المستخدمين",
+ "UserProfileWindowTitle": "مدير الملفات الشخصية للمستخدمين",
"CheatWindowTitle": "مدير الغش",
"DlcWindowTitle": "إدارة المحتوى القابل للتنزيل لـ {0} ({1})",
+ "ModWindowTitle": "إدارة التعديلات لـ {0} ({1})",
"UpdateWindowTitle": "مدير تحديث العنوان",
"CheatWindowHeading": "الغش متوفر لـ {0} [{1}]",
"BuildId": "معرف البناء:",
"DlcWindowHeading": "المحتويات القابلة للتنزيل {0}",
"ModWindowHeading": "{0} تعديل",
- "UserProfilesEditProfile": "تعديل المحددة",
+ "UserProfilesEditProfile": "تعديل المحدد",
"Cancel": "إلغاء",
"Save": "حفظ",
"Discard": "تجاهل",
"Paused": "متوقف مؤقتا",
- "UserProfilesSetProfileImage": "تعيين صورة ملف التعريف",
+ "UserProfilesSetProfileImage": "تعيين صورة الملف الشخصي",
"UserProfileEmptyNameError": "الاسم مطلوب",
"UserProfileNoImageError": "يجب تعيين صورة الملف الشخصي",
"GameUpdateWindowHeading": "إدارة التحديثات لـ {0} ({1})",
@@ -616,22 +723,22 @@
"UserProfilesName": "الاسم:",
"UserProfilesUserId": "معرف المستخدم:",
"SettingsTabGraphicsBackend": "خلفية الرسومات",
- "SettingsTabGraphicsBackendTooltip": "Select the graphics backend that will be used in the emulator.\n\nVulkan is overall better for all modern graphics cards, as long as their drivers are up to date. Vulkan also features faster shader compilation (less stuttering) on all GPU vendors.\n\nOpenGL may achieve better results on old Nvidia GPUs, on old AMD GPUs on Linux, or on GPUs with lower VRAM, though shader compilation stutters will be greater.\n\nSet to Vulkan if unsure. Set to OpenGL if your GPU does not support Vulkan even with the latest graphics drivers.",
+ "SettingsTabGraphicsBackendTooltip": "حدد الواجهة الخلفية للرسومات التي سيتم استخدامها في المحاكي.\n\nيعد برنامج فولكان أفضل بشكل عام لجميع بطاقات الرسومات الحديثة، طالما أن برامج التشغيل الخاصة بها محدثة. يتميز فولكان أيضا بتجميع مظللات أسرع (أقل تقطيعا) على جميع بائعي وحدات معالجة الرسومات.\n\nقد يحقق أوبن جي أل نتائج أفضل على وحدات معالجة الرسومات إنفيديا القديمة، أو على وحدات معالجة الرسومات إي إم دي القديمة على لينكس، أو على وحدات معالجة الرسومات ذات ذاكرة الوصول العشوائي للفيديوالأقل، على الرغم من أن تعثرات تجميع المظللات ستكون أكبر.\n\nاضبط على فولكان إذا لم تكن متأكدا. اضبط على أوبن جي أل إذا كانت وحدة معالجة الرسومات الخاصة بك لا تدعم فولكان حتى مع أحدث برامج تشغيل الرسومات.",
"SettingsEnableTextureRecompression": "تمكين إعادة ضغط التكستر",
- "SettingsEnableTextureRecompressionTooltip": "Compresses ASTC textures in order to reduce VRAM usage.\n\nGames using this texture format include Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder and The Legend of Zelda: Tears of the Kingdom.\n\nGraphics cards with 4GiB VRAM or less will likely crash at some point while running these games.\n\nEnable only if you're running out of VRAM on the aforementioned games. Leave OFF if unsure.",
- "SettingsTabGraphicsPreferredGpu": "GPU المفضل",
- "SettingsTabGraphicsPreferredGpuTooltip": "Select the graphics card that will be used with the Vulkan graphics backend.\n\nDoes not affect the GPU that OpenGL will use.\n\nSet to the GPU flagged as \"dGPU\" if unsure. If there isn't one, leave untouched.",
- "SettingsAppRequiredRestartMessage": "مطلوب إعادة تشغيل Ryujinx",
- "SettingsGpuBackendRestartMessage": "Graphics Backend or GPU settings have been modified. This will require a restart to be applied",
+ "SettingsEnableTextureRecompressionTooltip": "يضغط تكستر ASTC من أجل تقليل استخدام ذاكرة الوصول العشوائي للفيديو.\n\nتتضمن الألعاب التي تستخدم تنسيق النسيج هذا Astral Chain وBayonetta 3 وFire Emblem Engage وMetroid Prime Remastered وSuper Mario Bros. Wonder وThe Legend of Zelda: Tears of the Kingdom.\n\nمن المحتمل أن تتعطل بطاقات الرسومات التي تحتوي على 4 جيجا بايت من ذاكرة الوصول العشوائي للفيديو أو أقل في مرحلة ما أثناء تشغيل هذه الألعاب.\n\nقم بالتمكين فقط في حالة نفاد ذاكرة الوصول العشوائي للفيديو في الألعاب المذكورة أعلاه. اتركه معطلا إذا لم تكن متأكدا.",
+ "SettingsTabGraphicsPreferredGpu": "وحدة معالجة الرسوميات المفضلة",
+ "SettingsTabGraphicsPreferredGpuTooltip": "حدد بطاقة الرسومات التي سيتم استخدامها مع الواجهة الخلفية لرسومات فولكان.\n\nلا يؤثر على وحدة معالجة الرسومات التي سيستخدمها أوبن جي أل.\n\nاضبط على وحدة معالجة الرسومات التي تم وضع علامة عليها كـ \"dGPU\" إذا لم تكن متأكدًا. إذا لم يكن هناك واحد، اتركه.",
+ "SettingsAppRequiredRestartMessage": "مطلوب إعادة تشغيل ريوجينكس",
+ "SettingsGpuBackendRestartMessage": "تم تعديل إعدادات الواجهة الخلفية للرسومات أو وحدة معالجة الرسومات. سيتطلب هذا إعادة التشغيل ليتم تطبيقه",
"SettingsGpuBackendRestartSubMessage": "\n\nهل تريد إعادة التشغيل الآن؟",
- "RyujinxUpdaterMessage": "هل تريد تحديث Ryujinx إلى أحدث إصدار؟",
+ "RyujinxUpdaterMessage": "هل تريد تحديث ريوجينكس إلى أحدث إصدار؟",
"SettingsTabHotkeysVolumeUpHotkey": "زيادة مستوى الصوت:",
"SettingsTabHotkeysVolumeDownHotkey": "خفض مستوى الصوت:",
- "SettingsEnableMacroHLE": "Enable Macro HLE",
- "SettingsEnableMacroHLETooltip": "High-level emulation of GPU Macro code.\n\nImproves performance, but may cause graphical glitches in some games.\n\nLeave ON if unsure.",
- "SettingsEnableColorSpacePassthrough": "Color Space Passthrough",
- "SettingsEnableColorSpacePassthroughTooltip": "Directs the Vulkan backend to pass through color information without specifying a color space. For users with wide gamut displays, this may result in more vibrant colors, at the cost of color correctness.",
- "VolumeShort": "الحجم",
+ "SettingsEnableMacroHLE": "تمكين Maro HLE",
+ "SettingsEnableMacroHLETooltip": "محاكاة عالية المستوى لكود مايكرو وحدة معالجة الرسوميات.\n\nيعمل على تحسين الأداء، ولكنه قد يسبب خللا رسوميا في بعض الألعاب.\n\nاتركه مفعلا إذا لم تكن متأكدا.",
+ "SettingsEnableColorSpacePassthrough": "عبور مساحة اللون",
+ "SettingsEnableColorSpacePassthroughTooltip": "يوجه واجهة فولكان الخلفية لتمرير معلومات الألوان دون تحديد مساحة اللون. بالنسبة للمستخدمين الذين لديهم شاشات ذات نطاق واسع، قد يؤدي ذلك إلى الحصول على ألوان أكثر حيوية، على حساب صحة الألوان.",
+ "VolumeShort": "مستوى",
"UserProfilesManageSaves": "إدارة الحفظ",
"DeleteUserSave": "هل تريد حذف حفظ المستخدم لهذه اللعبة؟",
"IrreversibleActionNote": "هذا الإجراء لا يمكن التراجع عنه.",
@@ -642,12 +749,12 @@
"Search": "بحث",
"UserProfilesRecoverLostAccounts": "استعادة الحسابات المفقودة",
"Recover": "استعادة",
- "UserProfilesRecoverHeading": "تم العثور على الحفظ للحسابات التالية",
- "UserProfilesRecoverEmptyList": "لا توجد ملفات تعريف لاستردادها",
- "GraphicsAATooltip": "Applies anti-aliasing to the game render.\n\nFXAA will blur most of the image, while SMAA will attempt to find jagged edges and smooth them out.\n\nNot recommended to use in conjunction with the FSR scaling filter.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on NONE if unsure.",
+ "UserProfilesRecoverHeading": "تم العثور على حفظ للحسابات التالية",
+ "UserProfilesRecoverEmptyList": "لا توجد ملفات شخصية لاستردادها",
+ "GraphicsAATooltip": "يتم تطبيق تنعيم الحواف على عرض اللعبة.\n\nسوف يقوم FXAA بتعتيم معظم الصورة، بينما سيحاول SMAA العثور على حواف خشنة وتنعيمها.\n\nلا ينصح باستخدامه مع فلتر FSR لتكبير.\n\nيمكن تغيير هذا الخيار أثناء تشغيل اللعبة بالنقر فوق \"تطبيق\" أدناه؛ يمكنك ببساطة تحريك نافذة الإعدادات جانبا والتجربة حتى تجد المظهر المفضل للعبة.\n\nاتركه على لا شيء إذا لم تكن متأكدا.",
"GraphicsAALabel": "تنعيم الحواف:",
"GraphicsScalingFilterLabel": "فلتر التكبير:",
- "GraphicsScalingFilterTooltip": "Choose the scaling filter that will be applied when using resolution scale.\n\nBilinear works well for 3D games and is a safe default option.\n\nNearest is recommended for pixel art games.\n\nFSR 1.0 is merely a sharpening filter, not recommended for use with FXAA or SMAA.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on BILINEAR if unsure.",
+ "GraphicsScalingFilterTooltip": "اختر فلتر التكبير الذي سيتم تطبيقه عند استخدام مقياس الدقة.\n\nيعمل Bilinear بشكل جيد مع الألعاب ثلاثية الأبعاد وهو خيار افتراضي آمن.\n\nيوصى باستخدام Nearest لألعاب البكسل الفنية.\n\nFSR 1.0 هو مجرد مرشح توضيحي، ولا ينصح باستخدامه مع FXAA أو SMAA.\n\nيمكن تغيير هذا الخيار أثناء تشغيل اللعبة بالنقر فوق \"تطبيق\" أدناه؛ يمكنك ببساطة تحريك نافذة الإعدادات جانبا والتجربة حتى تجد المظهر المفضل للعبة.\n\nاتركه على Bilinear إذا لم تكن متأكدا.",
"GraphicsScalingFilterBilinear": "Bilinear",
"GraphicsScalingFilterNearest": "Nearest",
"GraphicsScalingFilterFsr": "FSR",
@@ -660,14 +767,14 @@
"UserEditorTitle": "تعديل المستخدم",
"UserEditorTitleCreate": "إنشاء مستخدم",
"SettingsTabNetworkInterface": "واجهة الشبكة:",
- "NetworkInterfaceTooltip": "The network interface used for LAN/LDN features.\n\nIn conjunction with a VPN or XLink Kai and a game with LAN support, can be used to spoof a same-network connection over the Internet.\n\nLeave on DEFAULT if unsure.",
+ "NetworkInterfaceTooltip": "واجهة الشبكة مستخدمة لميزات LAN/LDN.\n\nبالاشتراك مع VPN أو XLink Kai ولعبة تدعم LAN، يمكن استخدامها لتزييف اتصال الشبكة نفسها عبر الإنترنت.\n\nاتركه على الافتراضي إذا لم تكن متأكدا.",
"NetworkInterfaceDefault": "افتراضي",
- "PackagingShaders": "Packaging Shaders",
- "AboutChangelogButton": "عرض سجل التغييرات على GitHub",
+ "PackagingShaders": "تعبئة المظللات",
+ "AboutChangelogButton": "عرض سجل التغييرات على غيت هاب",
"AboutChangelogButtonTooltipMessage": "انقر لفتح سجل التغيير لهذا الإصدار في متصفحك الافتراضي.",
"SettingsTabNetworkMultiplayer": "لعب جماعي",
- "MultiplayerMode": "النمط:",
- "MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.",
+ "MultiplayerMode": "الوضع:",
+ "MultiplayerModeTooltip": "تغيير وضع LDN متعدد اللاعبين.\n\nسوف يقوم LdnMitm بتعديل وظيفة اللعب المحلية/اللاسلكية المحلية في الألعاب لتعمل كما لو كانت شبكة LAN، مما يسمح باتصالات الشبكة المحلية نفسها مع محاكيات ريوجينكس الأخرى وأجهزة نينتندو سويتش المخترقة التي تم تثبيت وحدة ldn_mitm عليها.\n\nيتطلب وضع اللاعبين المتعددين أن يكون جميع اللاعبين على نفس إصدار اللعبة (على سبيل المثال، يتعذر على الإصدار 13.0.1 من سوبر سماش برذرز ألتميت الاتصال بالإصدار 13.0.0).\n\nاتركه معطلا إذا لم تكن متأكدا.",
"MultiplayerModeDisabled": "معطل",
"MultiplayerModeLdnMitm": "ldn_mitm"
}
diff --git a/src/Ryujinx/Assets/Locales/de_DE.json b/src/Ryujinx/Assets/Locales/de_DE.json
index 71af4f3fe..401293198 100644
--- a/src/Ryujinx/Assets/Locales/de_DE.json
+++ b/src/Ryujinx/Assets/Locales/de_DE.json
@@ -30,6 +30,10 @@
"MenuBarToolsManageFileTypes": "Dateitypen verwalten",
"MenuBarToolsInstallFileTypes": "Dateitypen installieren",
"MenuBarToolsUninstallFileTypes": "Dateitypen deinstallieren",
+ "MenuBarView": "_Ansicht",
+ "MenuBarViewWindow": "Fenstergröße",
+ "MenuBarViewWindow720": "720p",
+ "MenuBarViewWindow1080": "1080p",
"MenuBarHelp": "_Hilfe",
"MenuBarHelpCheckForUpdates": "Nach Updates suchen",
"MenuBarHelpAbout": "Über Ryujinx",
@@ -92,6 +96,7 @@
"SettingsTabGeneralEnableDiscordRichPresence": "Aktiviere die Statusanzeige für Discord",
"SettingsTabGeneralCheckUpdatesOnLaunch": "Beim Start nach Updates suchen",
"SettingsTabGeneralShowConfirmExitDialog": "Zeige den \"Beenden bestätigen\"-Dialog",
+ "SettingsTabGeneralRememberWindowState": "Fenstergröße/-position merken",
"SettingsTabGeneralHideCursor": "Mauszeiger ausblenden",
"SettingsTabGeneralHideCursorNever": "Niemals",
"SettingsTabGeneralHideCursorOnIdle": "Mauszeiger bei Inaktivität ausblenden",
@@ -266,6 +271,107 @@
"ControllerSettingsMotionGyroDeadzone": "Gyro-Deadzone:",
"ControllerSettingsSave": "Speichern",
"ControllerSettingsClose": "Schließen",
+ "KeyUnknown": "Unbekannt",
+ "KeyShiftLeft": "Shift Left",
+ "KeyShiftRight": "Shift Right",
+ "KeyControlLeft": "Ctrl Left",
+ "KeyMacControlLeft": "⌃ Left",
+ "KeyControlRight": "Ctrl Right",
+ "KeyMacControlRight": "⌃ Right",
+ "KeyAltLeft": "Alt Left",
+ "KeyMacAltLeft": "⌥ Left",
+ "KeyAltRight": "Alt Right",
+ "KeyMacAltRight": "⌥ Right",
+ "KeyWinLeft": "⊞ Left",
+ "KeyMacWinLeft": "⌘ Left",
+ "KeyWinRight": "⊞ Right",
+ "KeyMacWinRight": "⌘ Right",
+ "KeyMenu": "Menu",
+ "KeyUp": "Up",
+ "KeyDown": "Down",
+ "KeyLeft": "Left",
+ "KeyRight": "Right",
+ "KeyEnter": "Enter",
+ "KeyEscape": "Escape",
+ "KeySpace": "Space",
+ "KeyTab": "Tab",
+ "KeyBackSpace": "Backspace",
+ "KeyInsert": "Insert",
+ "KeyDelete": "Delete",
+ "KeyPageUp": "Page Up",
+ "KeyPageDown": "Page Down",
+ "KeyHome": "Home",
+ "KeyEnd": "End",
+ "KeyCapsLock": "Caps Lock",
+ "KeyScrollLock": "Scroll Lock",
+ "KeyPrintScreen": "Print Screen",
+ "KeyPause": "Pause",
+ "KeyNumLock": "Num Lock",
+ "KeyClear": "Clear",
+ "KeyKeypad0": "Keypad 0",
+ "KeyKeypad1": "Keypad 1",
+ "KeyKeypad2": "Keypad 2",
+ "KeyKeypad3": "Keypad 3",
+ "KeyKeypad4": "Keypad 4",
+ "KeyKeypad5": "Keypad 5",
+ "KeyKeypad6": "Keypad 6",
+ "KeyKeypad7": "Keypad 7",
+ "KeyKeypad8": "Keypad 8",
+ "KeyKeypad9": "Keypad 9",
+ "KeyKeypadDivide": "Keypad Divide",
+ "KeyKeypadMultiply": "Keypad Multiply",
+ "KeyKeypadSubtract": "Keypad Subtract",
+ "KeyKeypadAdd": "Keypad Add",
+ "KeyKeypadDecimal": "Keypad Decimal",
+ "KeyKeypadEnter": "Keypad Enter",
+ "KeyNumber0": "0",
+ "KeyNumber1": "1",
+ "KeyNumber2": "2",
+ "KeyNumber3": "3",
+ "KeyNumber4": "4",
+ "KeyNumber5": "5",
+ "KeyNumber6": "6",
+ "KeyNumber7": "7",
+ "KeyNumber8": "8",
+ "KeyNumber9": "9",
+ "KeyTilde": "~",
+ "KeyGrave": "`",
+ "KeyMinus": "-",
+ "KeyPlus": "+",
+ "KeyBracketLeft": "[",
+ "KeyBracketRight": "]",
+ "KeySemicolon": ";",
+ "KeyQuote": "\"",
+ "KeyComma": ",",
+ "KeyPeriod": ".",
+ "KeySlash": "/",
+ "KeyBackSlash": "\\",
+ "KeyUnbound": "Unbound",
+ "GamepadLeftStick": "L Stick Button",
+ "GamepadRightStick": "R Stick Button",
+ "GamepadLeftShoulder": "Left Shoulder",
+ "GamepadRightShoulder": "Right Shoulder",
+ "GamepadLeftTrigger": "Left Trigger",
+ "GamepadRightTrigger": "Right Trigger",
+ "GamepadDpadUp": "Up",
+ "GamepadDpadDown": "Down",
+ "GamepadDpadLeft": "Left",
+ "GamepadDpadRight": "Right",
+ "GamepadMinus": "-",
+ "GamepadPlus": "+",
+ "GamepadGuide": "Guide",
+ "GamepadMisc1": "Misc",
+ "GamepadPaddle1": "Paddle 1",
+ "GamepadPaddle2": "Paddle 2",
+ "GamepadPaddle3": "Paddle 3",
+ "GamepadPaddle4": "Paddle 4",
+ "GamepadTouchpad": "Touchpad",
+ "GamepadSingleLeftTrigger0": "Left Trigger 0",
+ "GamepadSingleRightTrigger0": "Right Trigger 0",
+ "GamepadSingleLeftTrigger1": "Left Trigger 1",
+ "GamepadSingleRightTrigger1": "Right Trigger 1",
+ "StickLeft": "Left Stick",
+ "StickRight": "Right Stick",
"UserProfilesSelectedUserProfile": "Ausgewähltes Profil:",
"UserProfilesSaveProfileName": "Profilname speichern",
"UserProfilesChangeProfileImage": "Profilbild ändern",
@@ -597,6 +703,7 @@
"UserProfileWindowTitle": "Benutzerprofile verwalten",
"CheatWindowTitle": "Spiel-Cheats verwalten",
"DlcWindowTitle": "Spiel-DLC verwalten",
+ "ModWindowTitle": "Manage Mods for {0} ({1})",
"UpdateWindowTitle": "Spiel-Updates verwalten",
"CheatWindowHeading": "Cheats verfügbar für {0} [{1}]",
"BuildId": "BuildId:",
diff --git a/src/Ryujinx/Assets/Locales/el_GR.json b/src/Ryujinx/Assets/Locales/el_GR.json
index ba5c7078c..ccdf6e0e4 100644
--- a/src/Ryujinx/Assets/Locales/el_GR.json
+++ b/src/Ryujinx/Assets/Locales/el_GR.json
@@ -30,6 +30,10 @@
"MenuBarToolsManageFileTypes": "Διαχείριση τύπων αρχείων",
"MenuBarToolsInstallFileTypes": "Εγκαταστήσετε τύπους αρχείων.",
"MenuBarToolsUninstallFileTypes": "Απεγκαταστήσετε τύπους αρχείων",
+ "MenuBarView": "_View",
+ "MenuBarViewWindow": "Window Size",
+ "MenuBarViewWindow720": "720p",
+ "MenuBarViewWindow1080": "1080p",
"MenuBarHelp": "_Βοήθεια",
"MenuBarHelpCheckForUpdates": "Έλεγχος για Ενημερώσεις",
"MenuBarHelpAbout": "Σχετικά με",
@@ -92,6 +96,7 @@
"SettingsTabGeneralEnableDiscordRichPresence": "Ενεργοποίηση Εμπλουτισμένης Παρουσίας Discord",
"SettingsTabGeneralCheckUpdatesOnLaunch": "Έλεγχος για Ενημερώσεις στην Εκκίνηση",
"SettingsTabGeneralShowConfirmExitDialog": "Εμφάνιση διαλόγου \"Επιβεβαίωση Εξόδου\".",
+ "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position",
"SettingsTabGeneralHideCursor": "Απόκρυψη Κέρσορα:",
"SettingsTabGeneralHideCursorNever": "Ποτέ",
"SettingsTabGeneralHideCursorOnIdle": "Απόκρυψη Δρομέα στην Αδράνεια",
@@ -266,6 +271,107 @@
"ControllerSettingsMotionGyroDeadzone": "Νεκρή Ζώνη Γυροσκοπίου:",
"ControllerSettingsSave": "Αποθήκευση",
"ControllerSettingsClose": "Κλείσιμο",
+ "KeyUnknown": "Unknown",
+ "KeyShiftLeft": "Shift Left",
+ "KeyShiftRight": "Shift Right",
+ "KeyControlLeft": "Ctrl Left",
+ "KeyMacControlLeft": "⌃ Left",
+ "KeyControlRight": "Ctrl Right",
+ "KeyMacControlRight": "⌃ Right",
+ "KeyAltLeft": "Alt Left",
+ "KeyMacAltLeft": "⌥ Left",
+ "KeyAltRight": "Alt Right",
+ "KeyMacAltRight": "⌥ Right",
+ "KeyWinLeft": "⊞ Left",
+ "KeyMacWinLeft": "⌘ Left",
+ "KeyWinRight": "⊞ Right",
+ "KeyMacWinRight": "⌘ Right",
+ "KeyMenu": "Menu",
+ "KeyUp": "Up",
+ "KeyDown": "Down",
+ "KeyLeft": "Left",
+ "KeyRight": "Right",
+ "KeyEnter": "Enter",
+ "KeyEscape": "Escape",
+ "KeySpace": "Space",
+ "KeyTab": "Tab",
+ "KeyBackSpace": "Backspace",
+ "KeyInsert": "Insert",
+ "KeyDelete": "Delete",
+ "KeyPageUp": "Page Up",
+ "KeyPageDown": "Page Down",
+ "KeyHome": "Home",
+ "KeyEnd": "End",
+ "KeyCapsLock": "Caps Lock",
+ "KeyScrollLock": "Scroll Lock",
+ "KeyPrintScreen": "Print Screen",
+ "KeyPause": "Pause",
+ "KeyNumLock": "Num Lock",
+ "KeyClear": "Clear",
+ "KeyKeypad0": "Keypad 0",
+ "KeyKeypad1": "Keypad 1",
+ "KeyKeypad2": "Keypad 2",
+ "KeyKeypad3": "Keypad 3",
+ "KeyKeypad4": "Keypad 4",
+ "KeyKeypad5": "Keypad 5",
+ "KeyKeypad6": "Keypad 6",
+ "KeyKeypad7": "Keypad 7",
+ "KeyKeypad8": "Keypad 8",
+ "KeyKeypad9": "Keypad 9",
+ "KeyKeypadDivide": "Keypad Divide",
+ "KeyKeypadMultiply": "Keypad Multiply",
+ "KeyKeypadSubtract": "Keypad Subtract",
+ "KeyKeypadAdd": "Keypad Add",
+ "KeyKeypadDecimal": "Keypad Decimal",
+ "KeyKeypadEnter": "Keypad Enter",
+ "KeyNumber0": "0",
+ "KeyNumber1": "1",
+ "KeyNumber2": "2",
+ "KeyNumber3": "3",
+ "KeyNumber4": "4",
+ "KeyNumber5": "5",
+ "KeyNumber6": "6",
+ "KeyNumber7": "7",
+ "KeyNumber8": "8",
+ "KeyNumber9": "9",
+ "KeyTilde": "~",
+ "KeyGrave": "`",
+ "KeyMinus": "-",
+ "KeyPlus": "+",
+ "KeyBracketLeft": "[",
+ "KeyBracketRight": "]",
+ "KeySemicolon": ";",
+ "KeyQuote": "\"",
+ "KeyComma": ",",
+ "KeyPeriod": ".",
+ "KeySlash": "/",
+ "KeyBackSlash": "\\",
+ "KeyUnbound": "Unbound",
+ "GamepadLeftStick": "L Stick Button",
+ "GamepadRightStick": "R Stick Button",
+ "GamepadLeftShoulder": "Left Shoulder",
+ "GamepadRightShoulder": "Right Shoulder",
+ "GamepadLeftTrigger": "Left Trigger",
+ "GamepadRightTrigger": "Right Trigger",
+ "GamepadDpadUp": "Up",
+ "GamepadDpadDown": "Down",
+ "GamepadDpadLeft": "Left",
+ "GamepadDpadRight": "Right",
+ "GamepadMinus": "-",
+ "GamepadPlus": "+",
+ "GamepadGuide": "Guide",
+ "GamepadMisc1": "Misc",
+ "GamepadPaddle1": "Paddle 1",
+ "GamepadPaddle2": "Paddle 2",
+ "GamepadPaddle3": "Paddle 3",
+ "GamepadPaddle4": "Paddle 4",
+ "GamepadTouchpad": "Touchpad",
+ "GamepadSingleLeftTrigger0": "Left Trigger 0",
+ "GamepadSingleRightTrigger0": "Right Trigger 0",
+ "GamepadSingleLeftTrigger1": "Left Trigger 1",
+ "GamepadSingleRightTrigger1": "Right Trigger 1",
+ "StickLeft": "Left Stick",
+ "StickRight": "Right Stick",
"UserProfilesSelectedUserProfile": "Επιλεγμένο Προφίλ Χρήστη:",
"UserProfilesSaveProfileName": "Αποθήκευση Ονόματος Προφίλ",
"UserProfilesChangeProfileImage": "Αλλαγή Εικόνας Προφίλ",
@@ -597,6 +703,7 @@
"UserProfileWindowTitle": "Διαχειριστής Προφίλ Χρήστη",
"CheatWindowTitle": "Διαχειριστής των Cheats",
"DlcWindowTitle": "Downloadable Content Manager",
+ "ModWindowTitle": "Manage Mods for {0} ({1})",
"UpdateWindowTitle": "Διαχειριστής Ενημερώσεων Τίτλου",
"CheatWindowHeading": "Διαθέσιμα Cheats για {0} [{1}]",
"BuildId": "BuildId:",
diff --git a/src/Ryujinx/Assets/Locales/es_ES.json b/src/Ryujinx/Assets/Locales/es_ES.json
index 38499c046..e58fa5dcf 100644
--- a/src/Ryujinx/Assets/Locales/es_ES.json
+++ b/src/Ryujinx/Assets/Locales/es_ES.json
@@ -30,6 +30,10 @@
"MenuBarToolsManageFileTypes": "Administrar tipos de archivo",
"MenuBarToolsInstallFileTypes": "Instalar tipos de archivo",
"MenuBarToolsUninstallFileTypes": "Desinstalar tipos de archivo",
+ "MenuBarView": "_View",
+ "MenuBarViewWindow": "Window Size",
+ "MenuBarViewWindow720": "720p",
+ "MenuBarViewWindow1080": "1080p",
"MenuBarHelp": "_Ayuda",
"MenuBarHelpCheckForUpdates": "Buscar actualizaciones",
"MenuBarHelpAbout": "Acerca de",
@@ -92,6 +96,7 @@
"SettingsTabGeneralEnableDiscordRichPresence": "Habilitar estado en Discord",
"SettingsTabGeneralCheckUpdatesOnLaunch": "Buscar actualizaciones al iniciar",
"SettingsTabGeneralShowConfirmExitDialog": "Mostrar diálogo de confirmación al cerrar",
+ "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position",
"SettingsTabGeneralHideCursor": "Esconder el cursor:",
"SettingsTabGeneralHideCursorNever": "Nunca",
"SettingsTabGeneralHideCursorOnIdle": "Ocultar cursor cuando esté inactivo",
@@ -155,7 +160,7 @@
"SettingsTabGraphicsResolutionScaleNative": "Nativa (720p/1080p)",
"SettingsTabGraphicsResolutionScale2x": "x2 (1440p/2160p)",
"SettingsTabGraphicsResolutionScale3x": "x3 (2160p/3240p)",
- "SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p) (Not recommended)",
+ "SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p) (no recomendado)",
"SettingsTabGraphicsAspectRatio": "Relación de aspecto:",
"SettingsTabGraphicsAspectRatio4x3": "4:3",
"SettingsTabGraphicsAspectRatio16x9": "16:9",
@@ -266,6 +271,107 @@
"ControllerSettingsMotionGyroDeadzone": "Zona muerta de Gyro:",
"ControllerSettingsSave": "Guardar",
"ControllerSettingsClose": "Cerrar",
+ "KeyUnknown": "Desconocido",
+ "KeyShiftLeft": "Shift Left",
+ "KeyShiftRight": "Shift Right",
+ "KeyControlLeft": "Ctrl Left",
+ "KeyMacControlLeft": "⌃ Left",
+ "KeyControlRight": "Ctrl Right",
+ "KeyMacControlRight": "⌃ Right",
+ "KeyAltLeft": "Alt Left",
+ "KeyMacAltLeft": "⌥ Left",
+ "KeyAltRight": "Alt Right",
+ "KeyMacAltRight": "⌥ Right",
+ "KeyWinLeft": "⊞ Left",
+ "KeyMacWinLeft": "⌘ Left",
+ "KeyWinRight": "⊞ Right",
+ "KeyMacWinRight": "⌘ Right",
+ "KeyMenu": "Menu",
+ "KeyUp": "Up",
+ "KeyDown": "Down",
+ "KeyLeft": "Left",
+ "KeyRight": "Right",
+ "KeyEnter": "Enter",
+ "KeyEscape": "Escape",
+ "KeySpace": "Space",
+ "KeyTab": "Tab",
+ "KeyBackSpace": "Backspace",
+ "KeyInsert": "Insert",
+ "KeyDelete": "Delete",
+ "KeyPageUp": "Page Up",
+ "KeyPageDown": "Page Down",
+ "KeyHome": "Home",
+ "KeyEnd": "End",
+ "KeyCapsLock": "Caps Lock",
+ "KeyScrollLock": "Scroll Lock",
+ "KeyPrintScreen": "Print Screen",
+ "KeyPause": "Pause",
+ "KeyNumLock": "Num Lock",
+ "KeyClear": "Clear",
+ "KeyKeypad0": "Keypad 0",
+ "KeyKeypad1": "Keypad 1",
+ "KeyKeypad2": "Keypad 2",
+ "KeyKeypad3": "Keypad 3",
+ "KeyKeypad4": "Keypad 4",
+ "KeyKeypad5": "Keypad 5",
+ "KeyKeypad6": "Keypad 6",
+ "KeyKeypad7": "Keypad 7",
+ "KeyKeypad8": "Keypad 8",
+ "KeyKeypad9": "Keypad 9",
+ "KeyKeypadDivide": "Keypad Divide",
+ "KeyKeypadMultiply": "Keypad Multiply",
+ "KeyKeypadSubtract": "Keypad Subtract",
+ "KeyKeypadAdd": "Keypad Add",
+ "KeyKeypadDecimal": "Keypad Decimal",
+ "KeyKeypadEnter": "Keypad Enter",
+ "KeyNumber0": "0",
+ "KeyNumber1": "1",
+ "KeyNumber2": "2",
+ "KeyNumber3": "3",
+ "KeyNumber4": "4",
+ "KeyNumber5": "5",
+ "KeyNumber6": "6",
+ "KeyNumber7": "7",
+ "KeyNumber8": "8",
+ "KeyNumber9": "9",
+ "KeyTilde": "~",
+ "KeyGrave": "`",
+ "KeyMinus": "-",
+ "KeyPlus": "+",
+ "KeyBracketLeft": "[",
+ "KeyBracketRight": "]",
+ "KeySemicolon": ";",
+ "KeyQuote": "\"",
+ "KeyComma": ",",
+ "KeyPeriod": ".",
+ "KeySlash": "/",
+ "KeyBackSlash": "\\",
+ "KeyUnbound": "Unbound",
+ "GamepadLeftStick": "L Stick Button",
+ "GamepadRightStick": "R Stick Button",
+ "GamepadLeftShoulder": "Left Shoulder",
+ "GamepadRightShoulder": "Right Shoulder",
+ "GamepadLeftTrigger": "Left Trigger",
+ "GamepadRightTrigger": "Right Trigger",
+ "GamepadDpadUp": "Up",
+ "GamepadDpadDown": "Down",
+ "GamepadDpadLeft": "Left",
+ "GamepadDpadRight": "Right",
+ "GamepadMinus": "-",
+ "GamepadPlus": "+",
+ "GamepadGuide": "Guide",
+ "GamepadMisc1": "Misc",
+ "GamepadPaddle1": "Paddle 1",
+ "GamepadPaddle2": "Paddle 2",
+ "GamepadPaddle3": "Paddle 3",
+ "GamepadPaddle4": "Paddle 4",
+ "GamepadTouchpad": "Touchpad",
+ "GamepadSingleLeftTrigger0": "Left Trigger 0",
+ "GamepadSingleRightTrigger0": "Right Trigger 0",
+ "GamepadSingleLeftTrigger1": "Left Trigger 1",
+ "GamepadSingleRightTrigger1": "Right Trigger 1",
+ "StickLeft": "Left Stick",
+ "StickRight": "Right Stick",
"UserProfilesSelectedUserProfile": "Perfil de usuario seleccionado:",
"UserProfilesSaveProfileName": "Guardar nombre de perfil",
"UserProfilesChangeProfileImage": "Cambiar imagen de perfil",
@@ -597,6 +703,7 @@
"UserProfileWindowTitle": "Administrar perfiles de usuario",
"CheatWindowTitle": "Administrar cheats",
"DlcWindowTitle": "Administrar contenido descargable",
+ "ModWindowTitle": "Manage Mods for {0} ({1})",
"UpdateWindowTitle": "Administrar actualizaciones",
"CheatWindowHeading": "Cheats disponibles para {0} [{1}]",
"BuildId": "Id de compilación:",
@@ -647,12 +754,12 @@
"GraphicsAATooltip": "Applies anti-aliasing to the game render.\n\nFXAA will blur most of the image, while SMAA will attempt to find jagged edges and smooth them out.\n\nNot recommended to use in conjunction with the FSR scaling filter.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on NONE if unsure.",
"GraphicsAALabel": "Suavizado de bordes:",
"GraphicsScalingFilterLabel": "Filtro de escalado:",
- "GraphicsScalingFilterTooltip": "Choose the scaling filter that will be applied when using resolution scale.\n\nBilinear works well for 3D games and is a safe default option.\n\nNearest is recommended for pixel art games.\n\nFSR 1.0 is merely a sharpening filter, not recommended for use with FXAA or SMAA.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on BILINEAR if unsure.",
- "GraphicsScalingFilterBilinear": "Bilinear",
- "GraphicsScalingFilterNearest": "Nearest",
+ "GraphicsScalingFilterTooltip": "Elija el filtro de escala que se aplicará al utilizar la escala de resolución.\n\nBilinear funciona bien para juegos 3D y es una opción predeterminada segura.\n\nSe recomienda el bilinear para juegos de pixel art.\n\nFSR 1.0 es simplemente un filtro de afilado, no se recomienda su uso con FXAA o SMAA.\n\nEsta opción se puede cambiar mientras se ejecuta un juego haciendo clic en \"Aplicar\" a continuación; simplemente puedes mover la ventana de configuración a un lado y experimentar hasta que encuentres tu estilo preferido para un juego.\n\nDéjelo en BILINEAR si no está seguro.",
+ "GraphicsScalingFilterBilinear": "Bilinear\n",
+ "GraphicsScalingFilterNearest": "Cercano",
"GraphicsScalingFilterFsr": "FSR",
"GraphicsScalingFilterLevelLabel": "Nivel",
- "GraphicsScalingFilterLevelTooltip": "Set FSR 1.0 sharpening level. Higher is sharper.",
+ "GraphicsScalingFilterLevelTooltip": "Ajuste el nivel de nitidez FSR 1.0. Mayor es más nítido.",
"SmaaLow": "SMAA Bajo",
"SmaaMedium": "SMAA Medio",
"SmaaHigh": "SMAA Alto",
@@ -660,14 +767,14 @@
"UserEditorTitle": "Editar usuario",
"UserEditorTitleCreate": "Crear Usuario",
"SettingsTabNetworkInterface": "Interfaz de Red",
- "NetworkInterfaceTooltip": "The network interface used for LAN/LDN features.\n\nIn conjunction with a VPN or XLink Kai and a game with LAN support, can be used to spoof a same-network connection over the Internet.\n\nLeave on DEFAULT if unsure.",
+ "NetworkInterfaceTooltip": "Interfaz de red usada para características LAN/LDN.\n\njunto con una VPN o XLink Kai y un juego con soporte LAN, puede usarse para suplantar una conexión de la misma red a través de Internet.\n\nDeje en DEFAULT si no está seguro.",
"NetworkInterfaceDefault": "Predeterminado",
"PackagingShaders": "Empaquetando sombreadores",
"AboutChangelogButton": "Ver registro de cambios en GitHub",
"AboutChangelogButtonTooltipMessage": "Haga clic para abrir el registro de cambios para esta versión en su navegador predeterminado.",
"SettingsTabNetworkMultiplayer": "Multijugador",
"MultiplayerMode": "Modo:",
- "MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.",
- "MultiplayerModeDisabled": "Disabled",
+ "MultiplayerModeTooltip": "Cambiar modo LDN multijugador.\n\nLdnMitm modificará la funcionalidad local de juego inalámbrico para funcionar como si fuera LAN, permitiendo locales conexiones de la misma red con otras instancias de Ryujinx y consolas hackeadas de Nintendo Switch que tienen instalado el módulo ldn_mitm.\n\nMultijugador requiere que todos los jugadores estén en la misma versión del juego (por ejemplo, Super Smash Bros. Ultimate v13.0.1 no se puede conectar a v13.0.0).\n\nDejar DESACTIVADO si no está seguro.",
+ "MultiplayerModeDisabled": "Deshabilitar",
"MultiplayerModeLdnMitm": "ldn_mitm"
}
diff --git a/src/Ryujinx/Assets/Locales/fr_FR.json b/src/Ryujinx/Assets/Locales/fr_FR.json
index bb2864cf2..99a060650 100644
--- a/src/Ryujinx/Assets/Locales/fr_FR.json
+++ b/src/Ryujinx/Assets/Locales/fr_FR.json
@@ -30,6 +30,10 @@
"MenuBarToolsManageFileTypes": "Gérer les types de fichiers",
"MenuBarToolsInstallFileTypes": "Installer les types de fichiers",
"MenuBarToolsUninstallFileTypes": "Désinstaller les types de fichiers",
+ "MenuBarView": "_View",
+ "MenuBarViewWindow": "Window Size",
+ "MenuBarViewWindow720": "720p",
+ "MenuBarViewWindow1080": "1080p",
"MenuBarHelp": "_Aide",
"MenuBarHelpCheckForUpdates": "Vérifier les mises à jour",
"MenuBarHelpAbout": "À propos",
@@ -92,6 +96,7 @@
"SettingsTabGeneralEnableDiscordRichPresence": "Activer Discord Rich Presence",
"SettingsTabGeneralCheckUpdatesOnLaunch": "Vérifier les mises à jour au démarrage",
"SettingsTabGeneralShowConfirmExitDialog": "Afficher le message de \"Confirmation de sortie\"",
+ "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position",
"SettingsTabGeneralHideCursor": "Masquer le Curseur :",
"SettingsTabGeneralHideCursorNever": "Jamais",
"SettingsTabGeneralHideCursorOnIdle": "Masquer le curseur si inactif",
@@ -266,6 +271,107 @@
"ControllerSettingsMotionGyroDeadzone": "Zone morte du gyroscope:",
"ControllerSettingsSave": "Enregistrer",
"ControllerSettingsClose": "Fermer",
+ "KeyUnknown": "Unknown",
+ "KeyShiftLeft": "Shift Left",
+ "KeyShiftRight": "Shift Right",
+ "KeyControlLeft": "Ctrl Left",
+ "KeyMacControlLeft": "⌃ Left",
+ "KeyControlRight": "Ctrl Right",
+ "KeyMacControlRight": "⌃ Right",
+ "KeyAltLeft": "Alt Left",
+ "KeyMacAltLeft": "⌥ Left",
+ "KeyAltRight": "Alt Right",
+ "KeyMacAltRight": "⌥ Right",
+ "KeyWinLeft": "⊞ Left",
+ "KeyMacWinLeft": "⌘ Left",
+ "KeyWinRight": "⊞ Right",
+ "KeyMacWinRight": "⌘ Right",
+ "KeyMenu": "Menu",
+ "KeyUp": "Up",
+ "KeyDown": "Down",
+ "KeyLeft": "Left",
+ "KeyRight": "Right",
+ "KeyEnter": "Enter",
+ "KeyEscape": "Escape",
+ "KeySpace": "Space",
+ "KeyTab": "Tab",
+ "KeyBackSpace": "Backspace",
+ "KeyInsert": "Insert",
+ "KeyDelete": "Delete",
+ "KeyPageUp": "Page Up",
+ "KeyPageDown": "Page Down",
+ "KeyHome": "Home",
+ "KeyEnd": "End",
+ "KeyCapsLock": "Caps Lock",
+ "KeyScrollLock": "Scroll Lock",
+ "KeyPrintScreen": "Print Screen",
+ "KeyPause": "Pause",
+ "KeyNumLock": "Num Lock",
+ "KeyClear": "Clear",
+ "KeyKeypad0": "Keypad 0",
+ "KeyKeypad1": "Keypad 1",
+ "KeyKeypad2": "Keypad 2",
+ "KeyKeypad3": "Keypad 3",
+ "KeyKeypad4": "Keypad 4",
+ "KeyKeypad5": "Keypad 5",
+ "KeyKeypad6": "Keypad 6",
+ "KeyKeypad7": "Keypad 7",
+ "KeyKeypad8": "Keypad 8",
+ "KeyKeypad9": "Keypad 9",
+ "KeyKeypadDivide": "Keypad Divide",
+ "KeyKeypadMultiply": "Keypad Multiply",
+ "KeyKeypadSubtract": "Keypad Subtract",
+ "KeyKeypadAdd": "Keypad Add",
+ "KeyKeypadDecimal": "Keypad Decimal",
+ "KeyKeypadEnter": "Keypad Enter",
+ "KeyNumber0": "0",
+ "KeyNumber1": "1",
+ "KeyNumber2": "2",
+ "KeyNumber3": "3",
+ "KeyNumber4": "4",
+ "KeyNumber5": "5",
+ "KeyNumber6": "6",
+ "KeyNumber7": "7",
+ "KeyNumber8": "8",
+ "KeyNumber9": "9",
+ "KeyTilde": "~",
+ "KeyGrave": "`",
+ "KeyMinus": "-",
+ "KeyPlus": "+",
+ "KeyBracketLeft": "[",
+ "KeyBracketRight": "]",
+ "KeySemicolon": ";",
+ "KeyQuote": "\"",
+ "KeyComma": ",",
+ "KeyPeriod": ".",
+ "KeySlash": "/",
+ "KeyBackSlash": "\\",
+ "KeyUnbound": "Unbound",
+ "GamepadLeftStick": "L Stick Button",
+ "GamepadRightStick": "R Stick Button",
+ "GamepadLeftShoulder": "Left Shoulder",
+ "GamepadRightShoulder": "Right Shoulder",
+ "GamepadLeftTrigger": "Left Trigger",
+ "GamepadRightTrigger": "Right Trigger",
+ "GamepadDpadUp": "Up",
+ "GamepadDpadDown": "Down",
+ "GamepadDpadLeft": "Left",
+ "GamepadDpadRight": "Right",
+ "GamepadMinus": "-",
+ "GamepadPlus": "+",
+ "GamepadGuide": "Guide",
+ "GamepadMisc1": "Misc",
+ "GamepadPaddle1": "Paddle 1",
+ "GamepadPaddle2": "Paddle 2",
+ "GamepadPaddle3": "Paddle 3",
+ "GamepadPaddle4": "Paddle 4",
+ "GamepadTouchpad": "Touchpad",
+ "GamepadSingleLeftTrigger0": "Left Trigger 0",
+ "GamepadSingleRightTrigger0": "Right Trigger 0",
+ "GamepadSingleLeftTrigger1": "Left Trigger 1",
+ "GamepadSingleRightTrigger1": "Right Trigger 1",
+ "StickLeft": "Left Stick",
+ "StickRight": "Right Stick",
"UserProfilesSelectedUserProfile": "Profil utilisateur sélectionné :",
"UserProfilesSaveProfileName": "Enregistrer le nom du profil",
"UserProfilesChangeProfileImage": "Changer l'image du profil",
@@ -597,6 +703,7 @@
"UserProfileWindowTitle": "Gestionnaire de profils utilisateur",
"CheatWindowTitle": "Gestionnaire de triches",
"DlcWindowTitle": "Gérer le contenu téléchargeable pour {0} ({1})",
+ "ModWindowTitle": "Gérer les mods pour {0} ({1})",
"UpdateWindowTitle": "Gestionnaire de mises à jour",
"CheatWindowHeading": "Cheats disponibles pour {0} [{1}]",
"BuildId": "BuildId:",
@@ -648,8 +755,8 @@
"GraphicsAALabel": "Anticrénelage :",
"GraphicsScalingFilterLabel": "Filtre de mise à l'échelle :",
"GraphicsScalingFilterTooltip": "Choisissez le filtre de mise à l'échelle qui sera appliqué lors de l'utilisation de la mise à l'échelle de la résolution.\n\nLe filtre bilinéaire fonctionne bien pour les jeux en 3D et constitue une option par défaut sûre.\n\nLe filtre le plus proche est recommandé pour les jeux de pixel art.\n\nFSR 1.0 est simplement un filtre de netteté, non recommandé pour une utilisation avec FXAA ou SMAA.\n\nCette option peut être modifiée pendant qu'un jeu est en cours d'exécution en cliquant sur \"Appliquer\" ci-dessous ; vous pouvez simplement déplacer la fenêtre des paramètres de côté et expérimenter jusqu'à ce que vous trouviez l'aspect souhaité pour un jeu.\n\nLaissez sur BILINEAR si vous n'êtes pas sûr.",
- "GraphicsScalingFilterBilinear": "Bilinear",
- "GraphicsScalingFilterNearest": "Nearest",
+ "GraphicsScalingFilterBilinear": "Bilinéaire",
+ "GraphicsScalingFilterNearest": "Le plus proche",
"GraphicsScalingFilterFsr": "FSR",
"GraphicsScalingFilterLevelLabel": "Niveau ",
"GraphicsScalingFilterLevelTooltip": "Définissez le niveau de netteté FSR 1.0. Plus élevé signifie plus net.",
@@ -668,6 +775,6 @@
"SettingsTabNetworkMultiplayer": "Multijoueur",
"MultiplayerMode": "Mode :",
"MultiplayerModeTooltip": "Changer le mode multijoueur LDN.\n\nLdnMitm modifiera la fonctionnalité de jeu sans fil local/jeu local dans les jeux pour fonctionner comme s'il s'agissait d'un LAN, permettant des connexions locales sur le même réseau avec d'autres instances de Ryujinx et des consoles Nintendo Switch piratées ayant le module ldn_mitm installé.\n\nLe multijoueur nécessite que tous les joueurs soient sur la même version du jeu (par exemple, Super Smash Bros. Ultimate v13.0.1 ne peut pas se connecter à v13.0.0).\n\nLaissez DÉSACTIVÉ si vous n'êtes pas sûr.",
- "MultiplayerModeDisabled": "Disabled",
+ "MultiplayerModeDisabled": "Désactivé",
"MultiplayerModeLdnMitm": "ldn_mitm"
}
diff --git a/src/Ryujinx/Assets/Locales/he_IL.json b/src/Ryujinx/Assets/Locales/he_IL.json
index 16d68b775..848f78080 100644
--- a/src/Ryujinx/Assets/Locales/he_IL.json
+++ b/src/Ryujinx/Assets/Locales/he_IL.json
@@ -30,6 +30,10 @@
"MenuBarToolsManageFileTypes": "ניהול סוגי קבצים",
"MenuBarToolsInstallFileTypes": "סוגי קבצי התקנה",
"MenuBarToolsUninstallFileTypes": "סוגי קבצי הסרה",
+ "MenuBarView": "_View",
+ "MenuBarViewWindow": "Window Size",
+ "MenuBarViewWindow720": "720p",
+ "MenuBarViewWindow1080": "1080p",
"MenuBarHelp": "_עזרה",
"MenuBarHelpCheckForUpdates": "חפש עדכונים",
"MenuBarHelpAbout": "אודות",
@@ -92,6 +96,7 @@
"SettingsTabGeneralEnableDiscordRichPresence": "הפעלת תצוגה עשירה בדיסקורד",
"SettingsTabGeneralCheckUpdatesOnLaunch": "בדוק אם קיימים עדכונים בהפעלה",
"SettingsTabGeneralShowConfirmExitDialog": "הראה דיאלוג \"אשר יציאה\"",
+ "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position",
"SettingsTabGeneralHideCursor": "הסתר את הסמן",
"SettingsTabGeneralHideCursorNever": "אף פעם",
"SettingsTabGeneralHideCursorOnIdle": "במצב סרק",
@@ -266,6 +271,107 @@
"ControllerSettingsMotionGyroDeadzone": "שטח מת של הג'ירוסקופ:",
"ControllerSettingsSave": "שמירה",
"ControllerSettingsClose": "סגירה",
+ "KeyUnknown": "Unknown",
+ "KeyShiftLeft": "Shift Left",
+ "KeyShiftRight": "Shift Right",
+ "KeyControlLeft": "Ctrl Left",
+ "KeyMacControlLeft": "⌃ Left",
+ "KeyControlRight": "Ctrl Right",
+ "KeyMacControlRight": "⌃ Right",
+ "KeyAltLeft": "Alt Left",
+ "KeyMacAltLeft": "⌥ Left",
+ "KeyAltRight": "Alt Right",
+ "KeyMacAltRight": "⌥ Right",
+ "KeyWinLeft": "⊞ Left",
+ "KeyMacWinLeft": "⌘ Left",
+ "KeyWinRight": "⊞ Right",
+ "KeyMacWinRight": "⌘ Right",
+ "KeyMenu": "Menu",
+ "KeyUp": "Up",
+ "KeyDown": "Down",
+ "KeyLeft": "Left",
+ "KeyRight": "Right",
+ "KeyEnter": "Enter",
+ "KeyEscape": "Escape",
+ "KeySpace": "Space",
+ "KeyTab": "Tab",
+ "KeyBackSpace": "Backspace",
+ "KeyInsert": "Insert",
+ "KeyDelete": "Delete",
+ "KeyPageUp": "Page Up",
+ "KeyPageDown": "Page Down",
+ "KeyHome": "Home",
+ "KeyEnd": "End",
+ "KeyCapsLock": "Caps Lock",
+ "KeyScrollLock": "Scroll Lock",
+ "KeyPrintScreen": "Print Screen",
+ "KeyPause": "Pause",
+ "KeyNumLock": "Num Lock",
+ "KeyClear": "Clear",
+ "KeyKeypad0": "Keypad 0",
+ "KeyKeypad1": "Keypad 1",
+ "KeyKeypad2": "Keypad 2",
+ "KeyKeypad3": "Keypad 3",
+ "KeyKeypad4": "Keypad 4",
+ "KeyKeypad5": "Keypad 5",
+ "KeyKeypad6": "Keypad 6",
+ "KeyKeypad7": "Keypad 7",
+ "KeyKeypad8": "Keypad 8",
+ "KeyKeypad9": "Keypad 9",
+ "KeyKeypadDivide": "Keypad Divide",
+ "KeyKeypadMultiply": "Keypad Multiply",
+ "KeyKeypadSubtract": "Keypad Subtract",
+ "KeyKeypadAdd": "Keypad Add",
+ "KeyKeypadDecimal": "Keypad Decimal",
+ "KeyKeypadEnter": "Keypad Enter",
+ "KeyNumber0": "0",
+ "KeyNumber1": "1",
+ "KeyNumber2": "2",
+ "KeyNumber3": "3",
+ "KeyNumber4": "4",
+ "KeyNumber5": "5",
+ "KeyNumber6": "6",
+ "KeyNumber7": "7",
+ "KeyNumber8": "8",
+ "KeyNumber9": "9",
+ "KeyTilde": "~",
+ "KeyGrave": "`",
+ "KeyMinus": "-",
+ "KeyPlus": "+",
+ "KeyBracketLeft": "[",
+ "KeyBracketRight": "]",
+ "KeySemicolon": ";",
+ "KeyQuote": "\"",
+ "KeyComma": ",",
+ "KeyPeriod": ".",
+ "KeySlash": "/",
+ "KeyBackSlash": "\\",
+ "KeyUnbound": "Unbound",
+ "GamepadLeftStick": "L Stick Button",
+ "GamepadRightStick": "R Stick Button",
+ "GamepadLeftShoulder": "Left Shoulder",
+ "GamepadRightShoulder": "Right Shoulder",
+ "GamepadLeftTrigger": "Left Trigger",
+ "GamepadRightTrigger": "Right Trigger",
+ "GamepadDpadUp": "Up",
+ "GamepadDpadDown": "Down",
+ "GamepadDpadLeft": "Left",
+ "GamepadDpadRight": "Right",
+ "GamepadMinus": "-",
+ "GamepadPlus": "+",
+ "GamepadGuide": "Guide",
+ "GamepadMisc1": "Misc",
+ "GamepadPaddle1": "Paddle 1",
+ "GamepadPaddle2": "Paddle 2",
+ "GamepadPaddle3": "Paddle 3",
+ "GamepadPaddle4": "Paddle 4",
+ "GamepadTouchpad": "Touchpad",
+ "GamepadSingleLeftTrigger0": "Left Trigger 0",
+ "GamepadSingleRightTrigger0": "Right Trigger 0",
+ "GamepadSingleLeftTrigger1": "Left Trigger 1",
+ "GamepadSingleRightTrigger1": "Right Trigger 1",
+ "StickLeft": "Left Stick",
+ "StickRight": "Right Stick",
"UserProfilesSelectedUserProfile": "פרופיל המשתמש הנבחר:",
"UserProfilesSaveProfileName": "שמור שם פרופיל",
"UserProfilesChangeProfileImage": "שנה תמונת פרופיל",
@@ -597,6 +703,7 @@
"UserProfileWindowTitle": "ניהול פרופילי משתמש",
"CheatWindowTitle": "נהל צ'יטים למשחק",
"DlcWindowTitle": "נהל הרחבות משחק עבור {0} ({1})",
+ "ModWindowTitle": "Manage Mods for {0} ({1})",
"UpdateWindowTitle": "נהל עדכוני משחקים",
"CheatWindowHeading": "צ'יטים זמינים עבור {0} [{1}]",
"BuildId": "מזהה בניה:",
diff --git a/src/Ryujinx/Assets/Locales/it_IT.json b/src/Ryujinx/Assets/Locales/it_IT.json
index 0e832ffb6..280ebd880 100644
--- a/src/Ryujinx/Assets/Locales/it_IT.json
+++ b/src/Ryujinx/Assets/Locales/it_IT.json
@@ -2,7 +2,7 @@
"Language": "Italiano",
"MenuBarFileOpenApplet": "Apri applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "Apri l'applet Mii Editor in modalità Standalone",
- "SettingsTabInputDirectMouseAccess": "Accesso diretto mouse",
+ "SettingsTabInputDirectMouseAccess": "Accesso diretto al mouse",
"SettingsTabSystemMemoryManagerMode": "Modalità di gestione della memoria:",
"SettingsTabSystemMemoryManagerModeSoftware": "Software",
"SettingsTabSystemMemoryManagerModeHost": "Host (veloce)",
@@ -30,6 +30,10 @@
"MenuBarToolsManageFileTypes": "Gestisci i tipi di file",
"MenuBarToolsInstallFileTypes": "Installa i tipi di file",
"MenuBarToolsUninstallFileTypes": "Disinstalla i tipi di file",
+ "MenuBarView": "_View",
+ "MenuBarViewWindow": "Window Size",
+ "MenuBarViewWindow720": "720p",
+ "MenuBarViewWindow1080": "1080p",
"MenuBarHelp": "_Aiuto",
"MenuBarHelpCheckForUpdates": "Controlla aggiornamenti",
"MenuBarHelpAbout": "Informazioni",
@@ -92,6 +96,7 @@
"SettingsTabGeneralEnableDiscordRichPresence": "Attiva Discord Rich Presence",
"SettingsTabGeneralCheckUpdatesOnLaunch": "Controlla aggiornamenti all'avvio",
"SettingsTabGeneralShowConfirmExitDialog": "Mostra dialogo \"Conferma Uscita\"",
+ "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position",
"SettingsTabGeneralHideCursor": "Nascondi il cursore:",
"SettingsTabGeneralHideCursorNever": "Mai",
"SettingsTabGeneralHideCursorOnIdle": "Quando è inattivo",
@@ -266,6 +271,107 @@
"ControllerSettingsMotionGyroDeadzone": "Zona morta del giroscopio:",
"ControllerSettingsSave": "Salva",
"ControllerSettingsClose": "Chiudi",
+ "KeyUnknown": "Sconosciuto",
+ "KeyShiftLeft": "Maiusc sinistro",
+ "KeyShiftRight": "Maiusc destro",
+ "KeyControlLeft": "Ctrl sinistro",
+ "KeyMacControlLeft": "⌃ sinistro",
+ "KeyControlRight": "Ctrl destro",
+ "KeyMacControlRight": "⌃ destro",
+ "KeyAltLeft": "Alt sinistro",
+ "KeyMacAltLeft": "⌥ sinistro",
+ "KeyAltRight": "Alt destro",
+ "KeyMacAltRight": "⌥ destro",
+ "KeyWinLeft": "⊞ sinistro",
+ "KeyMacWinLeft": "⌘ sinistro",
+ "KeyWinRight": "⊞ destro",
+ "KeyMacWinRight": "⌘ destro",
+ "KeyMenu": "Menù",
+ "KeyUp": "Su",
+ "KeyDown": "Giù",
+ "KeyLeft": "Sinistra",
+ "KeyRight": "Destra",
+ "KeyEnter": "Invio",
+ "KeyEscape": "Esc",
+ "KeySpace": "Spazio",
+ "KeyTab": "Tab",
+ "KeyBackSpace": "Backspace",
+ "KeyInsert": "Ins",
+ "KeyDelete": "Canc",
+ "KeyPageUp": "Pag. Su",
+ "KeyPageDown": "Pag. Giù",
+ "KeyHome": "Inizio",
+ "KeyEnd": "Fine",
+ "KeyCapsLock": "Bloc Maiusc",
+ "KeyScrollLock": "Bloc Scorr",
+ "KeyPrintScreen": "Stamp",
+ "KeyPause": "Pausa",
+ "KeyNumLock": "Bloc Num",
+ "KeyClear": "Clear",
+ "KeyKeypad0": "Tast. num. 0",
+ "KeyKeypad1": "Tast. num. 1",
+ "KeyKeypad2": "Tast. num. 2",
+ "KeyKeypad3": "Tast. num. 3",
+ "KeyKeypad4": "Tast. num. 4",
+ "KeyKeypad5": "Tast. num. 5",
+ "KeyKeypad6": "Tast. num. 6",
+ "KeyKeypad7": "Tast. num. 7",
+ "KeyKeypad8": "Tast. num. 8",
+ "KeyKeypad9": "Tast. num. 9",
+ "KeyKeypadDivide": "Tast. num. /",
+ "KeyKeypadMultiply": "Tast. num. *",
+ "KeyKeypadSubtract": "Tast. num. -",
+ "KeyKeypadAdd": "Tast. num. +",
+ "KeyKeypadDecimal": "Tast. num. sep. decimale",
+ "KeyKeypadEnter": "Tast. num. Invio",
+ "KeyNumber0": "0",
+ "KeyNumber1": "1",
+ "KeyNumber2": "2",
+ "KeyNumber3": "3",
+ "KeyNumber4": "4",
+ "KeyNumber5": "5",
+ "KeyNumber6": "6",
+ "KeyNumber7": "7",
+ "KeyNumber8": "8",
+ "KeyNumber9": "9",
+ "KeyTilde": "ò",
+ "KeyGrave": "`",
+ "KeyMinus": "-",
+ "KeyPlus": "+",
+ "KeyBracketLeft": "'",
+ "KeyBracketRight": "ì",
+ "KeySemicolon": "è",
+ "KeyQuote": "à",
+ "KeyComma": ",",
+ "KeyPeriod": ".",
+ "KeySlash": "ù",
+ "KeyBackSlash": "<",
+ "KeyUnbound": "Non assegnato",
+ "GamepadLeftStick": "Pulsante levetta sinistra",
+ "GamepadRightStick": "Pulsante levetta destra",
+ "GamepadLeftShoulder": "Pulsante dorsale sinistro",
+ "GamepadRightShoulder": "Pulsante dorsale destro",
+ "GamepadLeftTrigger": "Grilletto sinistro",
+ "GamepadRightTrigger": "Grilletto destro",
+ "GamepadDpadUp": "Su",
+ "GamepadDpadDown": "Giù",
+ "GamepadDpadLeft": "Sinistra",
+ "GamepadDpadRight": "Destra",
+ "GamepadMinus": "-",
+ "GamepadPlus": "+",
+ "GamepadGuide": "Guide",
+ "GamepadMisc1": "Misc",
+ "GamepadPaddle1": "Paddle 1",
+ "GamepadPaddle2": "Paddle 2",
+ "GamepadPaddle3": "Paddle 3",
+ "GamepadPaddle4": "Paddle 4",
+ "GamepadTouchpad": "Touchpad",
+ "GamepadSingleLeftTrigger0": "Grilletto sinistro 0",
+ "GamepadSingleRightTrigger0": "Grilletto destro 0",
+ "GamepadSingleLeftTrigger1": "Grilletto sinistro 1",
+ "GamepadSingleRightTrigger1": "Grilletto destro 1",
+ "StickLeft": "Levetta sinistra",
+ "StickRight": "Levetta destra",
"UserProfilesSelectedUserProfile": "Profilo utente selezionato:",
"UserProfilesSaveProfileName": "Salva nome del profilo",
"UserProfilesChangeProfileImage": "Cambia immagine profilo",
@@ -597,6 +703,7 @@
"UserProfileWindowTitle": "Gestione profili utente",
"CheatWindowTitle": "Gestione trucchi",
"DlcWindowTitle": "Gestisci DLC per {0} ({1})",
+ "ModWindowTitle": "Gestisci mod per {0} ({1})",
"UpdateWindowTitle": "Gestione aggiornamenti",
"CheatWindowHeading": "Trucchi disponibili per {0} [{1}]",
"BuildId": "ID Build",
diff --git a/src/Ryujinx/Assets/Locales/ja_JP.json b/src/Ryujinx/Assets/Locales/ja_JP.json
index 8790135e0..61e963258 100644
--- a/src/Ryujinx/Assets/Locales/ja_JP.json
+++ b/src/Ryujinx/Assets/Locales/ja_JP.json
@@ -30,6 +30,10 @@
"MenuBarToolsManageFileTypes": "ファイル形式を管理",
"MenuBarToolsInstallFileTypes": "ファイル形式をインストール",
"MenuBarToolsUninstallFileTypes": "ファイル形式をアンインストール",
+ "MenuBarView": "_View",
+ "MenuBarViewWindow": "Window Size",
+ "MenuBarViewWindow720": "720p",
+ "MenuBarViewWindow1080": "1080p",
"MenuBarHelp": "ヘルプ(_H)",
"MenuBarHelpCheckForUpdates": "アップデートを確認",
"MenuBarHelpAbout": "Ryujinx について",
@@ -92,6 +96,7 @@
"SettingsTabGeneralEnableDiscordRichPresence": "Discord リッチプレゼンスを有効にする",
"SettingsTabGeneralCheckUpdatesOnLaunch": "起動時にアップデートを確認する",
"SettingsTabGeneralShowConfirmExitDialog": "\"終了を確認\" ダイアログを表示する",
+ "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position",
"SettingsTabGeneralHideCursor": "マウスカーソルを非表示",
"SettingsTabGeneralHideCursorNever": "決して",
"SettingsTabGeneralHideCursorOnIdle": "アイドル時",
@@ -266,6 +271,107 @@
"ControllerSettingsMotionGyroDeadzone": "ジャイロ遊び:",
"ControllerSettingsSave": "セーブ",
"ControllerSettingsClose": "閉じる",
+ "KeyUnknown": "Unknown",
+ "KeyShiftLeft": "Shift Left",
+ "KeyShiftRight": "Shift Right",
+ "KeyControlLeft": "Ctrl Left",
+ "KeyMacControlLeft": "⌃ Left",
+ "KeyControlRight": "Ctrl Right",
+ "KeyMacControlRight": "⌃ Right",
+ "KeyAltLeft": "Alt Left",
+ "KeyMacAltLeft": "⌥ Left",
+ "KeyAltRight": "Alt Right",
+ "KeyMacAltRight": "⌥ Right",
+ "KeyWinLeft": "⊞ Left",
+ "KeyMacWinLeft": "⌘ Left",
+ "KeyWinRight": "⊞ Right",
+ "KeyMacWinRight": "⌘ Right",
+ "KeyMenu": "Menu",
+ "KeyUp": "Up",
+ "KeyDown": "Down",
+ "KeyLeft": "Left",
+ "KeyRight": "Right",
+ "KeyEnter": "Enter",
+ "KeyEscape": "Escape",
+ "KeySpace": "Space",
+ "KeyTab": "Tab",
+ "KeyBackSpace": "Backspace",
+ "KeyInsert": "Insert",
+ "KeyDelete": "Delete",
+ "KeyPageUp": "Page Up",
+ "KeyPageDown": "Page Down",
+ "KeyHome": "Home",
+ "KeyEnd": "End",
+ "KeyCapsLock": "Caps Lock",
+ "KeyScrollLock": "Scroll Lock",
+ "KeyPrintScreen": "Print Screen",
+ "KeyPause": "Pause",
+ "KeyNumLock": "Num Lock",
+ "KeyClear": "Clear",
+ "KeyKeypad0": "Keypad 0",
+ "KeyKeypad1": "Keypad 1",
+ "KeyKeypad2": "Keypad 2",
+ "KeyKeypad3": "Keypad 3",
+ "KeyKeypad4": "Keypad 4",
+ "KeyKeypad5": "Keypad 5",
+ "KeyKeypad6": "Keypad 6",
+ "KeyKeypad7": "Keypad 7",
+ "KeyKeypad8": "Keypad 8",
+ "KeyKeypad9": "Keypad 9",
+ "KeyKeypadDivide": "Keypad Divide",
+ "KeyKeypadMultiply": "Keypad Multiply",
+ "KeyKeypadSubtract": "Keypad Subtract",
+ "KeyKeypadAdd": "Keypad Add",
+ "KeyKeypadDecimal": "Keypad Decimal",
+ "KeyKeypadEnter": "Keypad Enter",
+ "KeyNumber0": "0",
+ "KeyNumber1": "1",
+ "KeyNumber2": "2",
+ "KeyNumber3": "3",
+ "KeyNumber4": "4",
+ "KeyNumber5": "5",
+ "KeyNumber6": "6",
+ "KeyNumber7": "7",
+ "KeyNumber8": "8",
+ "KeyNumber9": "9",
+ "KeyTilde": "~",
+ "KeyGrave": "`",
+ "KeyMinus": "-",
+ "KeyPlus": "+",
+ "KeyBracketLeft": "[",
+ "KeyBracketRight": "]",
+ "KeySemicolon": ";",
+ "KeyQuote": "\"",
+ "KeyComma": ",",
+ "KeyPeriod": ".",
+ "KeySlash": "/",
+ "KeyBackSlash": "\\",
+ "KeyUnbound": "Unbound",
+ "GamepadLeftStick": "L Stick Button",
+ "GamepadRightStick": "R Stick Button",
+ "GamepadLeftShoulder": "Left Shoulder",
+ "GamepadRightShoulder": "Right Shoulder",
+ "GamepadLeftTrigger": "Left Trigger",
+ "GamepadRightTrigger": "Right Trigger",
+ "GamepadDpadUp": "Up",
+ "GamepadDpadDown": "Down",
+ "GamepadDpadLeft": "Left",
+ "GamepadDpadRight": "Right",
+ "GamepadMinus": "-",
+ "GamepadPlus": "+",
+ "GamepadGuide": "Guide",
+ "GamepadMisc1": "Misc",
+ "GamepadPaddle1": "Paddle 1",
+ "GamepadPaddle2": "Paddle 2",
+ "GamepadPaddle3": "Paddle 3",
+ "GamepadPaddle4": "Paddle 4",
+ "GamepadTouchpad": "Touchpad",
+ "GamepadSingleLeftTrigger0": "Left Trigger 0",
+ "GamepadSingleRightTrigger0": "Right Trigger 0",
+ "GamepadSingleLeftTrigger1": "Left Trigger 1",
+ "GamepadSingleRightTrigger1": "Right Trigger 1",
+ "StickLeft": "Left Stick",
+ "StickRight": "Right Stick",
"UserProfilesSelectedUserProfile": "選択されたユーザプロファイル:",
"UserProfilesSaveProfileName": "プロファイル名をセーブ",
"UserProfilesChangeProfileImage": "プロファイル画像を変更",
@@ -597,6 +703,7 @@
"UserProfileWindowTitle": "ユーザプロファイルを管理",
"CheatWindowTitle": "チート管理",
"DlcWindowTitle": "DLC 管理",
+ "ModWindowTitle": "Manage Mods for {0} ({1})",
"UpdateWindowTitle": "アップデート管理",
"CheatWindowHeading": "利用可能なチート {0} [{1}]",
"BuildId": "ビルドID:",
diff --git a/src/Ryujinx/Assets/Locales/ko_KR.json b/src/Ryujinx/Assets/Locales/ko_KR.json
index 0cd054cab..a92d084e0 100644
--- a/src/Ryujinx/Assets/Locales/ko_KR.json
+++ b/src/Ryujinx/Assets/Locales/ko_KR.json
@@ -30,6 +30,10 @@
"MenuBarToolsManageFileTypes": "파일 형식 관리",
"MenuBarToolsInstallFileTypes": "파일 형식 설치",
"MenuBarToolsUninstallFileTypes": "파일 형식 설치 제거",
+ "MenuBarView": "_보기",
+ "MenuBarViewWindow": "창 크기",
+ "MenuBarViewWindow720": "720p",
+ "MenuBarViewWindow1080": "1080p",
"MenuBarHelp": "도움말(_H)",
"MenuBarHelpCheckForUpdates": "업데이트 확인",
"MenuBarHelpAbout": "정보",
@@ -92,6 +96,7 @@
"SettingsTabGeneralEnableDiscordRichPresence": "디스코드 활동 상태 활성화",
"SettingsTabGeneralCheckUpdatesOnLaunch": "시작 시, 업데이트 확인",
"SettingsTabGeneralShowConfirmExitDialog": "\"종료 확인\" 대화 상자 표시",
+ "SettingsTabGeneralRememberWindowState": "창 크기/위치 기억",
"SettingsTabGeneralHideCursor": "마우스 커서 숨기기",
"SettingsTabGeneralHideCursorNever": "절대 안 함",
"SettingsTabGeneralHideCursorOnIdle": "유휴 상태",
@@ -266,6 +271,107 @@
"ControllerSettingsMotionGyroDeadzone": "자이로 사각지대 :",
"ControllerSettingsSave": "저장",
"ControllerSettingsClose": "닫기",
+ "KeyUnknown": "알 수 없음",
+ "KeyShiftLeft": "왼쪽 Shift",
+ "KeyShiftRight": "오른쪽 Shift",
+ "KeyControlLeft": "왼쪽 Ctrl",
+ "KeyMacControlLeft": "왼쪽 ^",
+ "KeyControlRight": "오른쪽 Ctrl",
+ "KeyMacControlRight": "오른쪽 ^",
+ "KeyAltLeft": "왼쪽 Alt",
+ "KeyMacAltLeft": "왼쪽 ⌥",
+ "KeyAltRight": "오른쪽 Alt",
+ "KeyMacAltRight": "오른쪽 ⌥",
+ "KeyWinLeft": "왼쪽 ⊞",
+ "KeyMacWinLeft": "왼쪽 ⌘",
+ "KeyWinRight": "오른쪽 ⊞",
+ "KeyMacWinRight": "오른쪽 ⌘",
+ "KeyMenu": "메뉴",
+ "KeyUp": "↑",
+ "KeyDown": "↓",
+ "KeyLeft": "←",
+ "KeyRight": "→",
+ "KeyEnter": "엔터",
+ "KeyEscape": "이스케이프",
+ "KeySpace": "스페이스",
+ "KeyTab": "탭",
+ "KeyBackSpace": "백스페이스",
+ "KeyInsert": "Ins",
+ "KeyDelete": "Del",
+ "KeyPageUp": "Page Up",
+ "KeyPageDown": "Page Down",
+ "KeyHome": "Home",
+ "KeyEnd": "End",
+ "KeyCapsLock": "Caps Lock",
+ "KeyScrollLock": "Scroll Lock",
+ "KeyPrintScreen": "프린트 스크린",
+ "KeyPause": "Pause",
+ "KeyNumLock": "Num Lock",
+ "KeyClear": "지우기",
+ "KeyKeypad0": "키패드 0",
+ "KeyKeypad1": "키패드 1",
+ "KeyKeypad2": "키패드 2",
+ "KeyKeypad3": "키패드 3",
+ "KeyKeypad4": "키패드 4",
+ "KeyKeypad5": "키패드 5",
+ "KeyKeypad6": "키패드 6",
+ "KeyKeypad7": "키패드 7",
+ "KeyKeypad8": "키패드 8",
+ "KeyKeypad9": "키패드 9",
+ "KeyKeypadDivide": "키패드 분할",
+ "KeyKeypadMultiply": "키패드 멀티플",
+ "KeyKeypadSubtract": "키패드 빼기",
+ "KeyKeypadAdd": "키패드 추가",
+ "KeyKeypadDecimal": "숫자 키패드",
+ "KeyKeypadEnter": "키패드 엔터",
+ "KeyNumber0": "0",
+ "KeyNumber1": "1",
+ "KeyNumber2": "2",
+ "KeyNumber3": "3",
+ "KeyNumber4": "4",
+ "KeyNumber5": "5",
+ "KeyNumber6": "6",
+ "KeyNumber7": "7",
+ "KeyNumber8": "8",
+ "KeyNumber9": "9",
+ "KeyTilde": "~",
+ "KeyGrave": "`",
+ "KeyMinus": "-",
+ "KeyPlus": "+",
+ "KeyBracketLeft": "[",
+ "KeyBracketRight": "]",
+ "KeySemicolon": ";",
+ "KeyQuote": "\"",
+ "KeyComma": ",",
+ "KeyPeriod": ".",
+ "KeySlash": "/",
+ "KeyBackSlash": "\\",
+ "KeyUnbound": "바인딩 해제",
+ "GamepadLeftStick": "L 스틱 버튼",
+ "GamepadRightStick": "R 스틱 버튼",
+ "GamepadLeftShoulder": "좌측 숄더",
+ "GamepadRightShoulder": "우측 숄더",
+ "GamepadLeftTrigger": "좌측 트리거",
+ "GamepadRightTrigger": "우측 트리거",
+ "GamepadDpadUp": "↑",
+ "GamepadDpadDown": "↓",
+ "GamepadDpadLeft": "←",
+ "GamepadDpadRight": "→",
+ "GamepadMinus": "-",
+ "GamepadPlus": "+",
+ "GamepadGuide": "안내",
+ "GamepadMisc1": "기타",
+ "GamepadPaddle1": "패들 1",
+ "GamepadPaddle2": "패들 2",
+ "GamepadPaddle3": "패들 3",
+ "GamepadPaddle4": "패들 4",
+ "GamepadTouchpad": "터치패드",
+ "GamepadSingleLeftTrigger0": "왼쪽 트리거 0",
+ "GamepadSingleRightTrigger0": "오른쪽 트리거 0",
+ "GamepadSingleLeftTrigger1": "왼쪽 트리거 1",
+ "GamepadSingleRightTrigger1": "오른쪽 트리거 1",
+ "StickLeft": "좌측 스틱",
+ "StickRight": "우측 스틱",
"UserProfilesSelectedUserProfile": "선택한 사용자 프로필 :",
"UserProfilesSaveProfileName": "프로필 이름 저장",
"UserProfilesChangeProfileImage": "프로필 이미지 변경",
@@ -597,6 +703,7 @@
"UserProfileWindowTitle": "사용자 프로파일 관리자",
"CheatWindowTitle": "치트 관리자",
"DlcWindowTitle": "{0} ({1})의 다운로드 가능한 콘텐츠 관리",
+ "ModWindowTitle": "{0} ({1})의 Mod 관리",
"UpdateWindowTitle": "타이틀 업데이트 관리자",
"CheatWindowHeading": "{0} [{1}]에 사용할 수 있는 치트",
"BuildId": "빌드ID :",
diff --git a/src/Ryujinx/Assets/Locales/pl_PL.json b/src/Ryujinx/Assets/Locales/pl_PL.json
index 94096dd7b..9d1bd7b44 100644
--- a/src/Ryujinx/Assets/Locales/pl_PL.json
+++ b/src/Ryujinx/Assets/Locales/pl_PL.json
@@ -30,6 +30,10 @@
"MenuBarToolsManageFileTypes": "Zarządzaj rodzajami plików",
"MenuBarToolsInstallFileTypes": "Typy plików instalacyjnych",
"MenuBarToolsUninstallFileTypes": "Typy plików dezinstalacyjnych",
+ "MenuBarView": "_View",
+ "MenuBarViewWindow": "Window Size",
+ "MenuBarViewWindow720": "720p",
+ "MenuBarViewWindow1080": "1080p",
"MenuBarHelp": "_Pomoc",
"MenuBarHelpCheckForUpdates": "Sprawdź aktualizacje",
"MenuBarHelpAbout": "O programie",
@@ -92,6 +96,7 @@
"SettingsTabGeneralEnableDiscordRichPresence": "Włącz Bogatą Obecność Discord",
"SettingsTabGeneralCheckUpdatesOnLaunch": "Sprawdzaj aktualizacje przy uruchomieniu",
"SettingsTabGeneralShowConfirmExitDialog": "Pokazuj okno dialogowe \"Potwierdź wyjście\"",
+ "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position",
"SettingsTabGeneralHideCursor": "Ukryj kursor:",
"SettingsTabGeneralHideCursorNever": "Nigdy",
"SettingsTabGeneralHideCursorOnIdle": "Gdy bezczynny",
@@ -266,6 +271,107 @@
"ControllerSettingsMotionGyroDeadzone": "Deadzone Żyroskopu:",
"ControllerSettingsSave": "Zapisz",
"ControllerSettingsClose": "Zamknij",
+ "KeyUnknown": "Unknown",
+ "KeyShiftLeft": "Shift Left",
+ "KeyShiftRight": "Shift Right",
+ "KeyControlLeft": "Ctrl Left",
+ "KeyMacControlLeft": "⌃ Left",
+ "KeyControlRight": "Ctrl Right",
+ "KeyMacControlRight": "⌃ Right",
+ "KeyAltLeft": "Alt Left",
+ "KeyMacAltLeft": "⌥ Left",
+ "KeyAltRight": "Alt Right",
+ "KeyMacAltRight": "⌥ Right",
+ "KeyWinLeft": "⊞ Left",
+ "KeyMacWinLeft": "⌘ Left",
+ "KeyWinRight": "⊞ Right",
+ "KeyMacWinRight": "⌘ Right",
+ "KeyMenu": "Menu",
+ "KeyUp": "Up",
+ "KeyDown": "Down",
+ "KeyLeft": "Left",
+ "KeyRight": "Right",
+ "KeyEnter": "Enter",
+ "KeyEscape": "Escape",
+ "KeySpace": "Space",
+ "KeyTab": "Tab",
+ "KeyBackSpace": "Backspace",
+ "KeyInsert": "Insert",
+ "KeyDelete": "Delete",
+ "KeyPageUp": "Page Up",
+ "KeyPageDown": "Page Down",
+ "KeyHome": "Home",
+ "KeyEnd": "End",
+ "KeyCapsLock": "Caps Lock",
+ "KeyScrollLock": "Scroll Lock",
+ "KeyPrintScreen": "Print Screen",
+ "KeyPause": "Pause",
+ "KeyNumLock": "Num Lock",
+ "KeyClear": "Clear",
+ "KeyKeypad0": "Keypad 0",
+ "KeyKeypad1": "Keypad 1",
+ "KeyKeypad2": "Keypad 2",
+ "KeyKeypad3": "Keypad 3",
+ "KeyKeypad4": "Keypad 4",
+ "KeyKeypad5": "Keypad 5",
+ "KeyKeypad6": "Keypad 6",
+ "KeyKeypad7": "Keypad 7",
+ "KeyKeypad8": "Keypad 8",
+ "KeyKeypad9": "Keypad 9",
+ "KeyKeypadDivide": "Keypad Divide",
+ "KeyKeypadMultiply": "Keypad Multiply",
+ "KeyKeypadSubtract": "Keypad Subtract",
+ "KeyKeypadAdd": "Keypad Add",
+ "KeyKeypadDecimal": "Keypad Decimal",
+ "KeyKeypadEnter": "Keypad Enter",
+ "KeyNumber0": "0",
+ "KeyNumber1": "1",
+ "KeyNumber2": "2",
+ "KeyNumber3": "3",
+ "KeyNumber4": "4",
+ "KeyNumber5": "5",
+ "KeyNumber6": "6",
+ "KeyNumber7": "7",
+ "KeyNumber8": "8",
+ "KeyNumber9": "9",
+ "KeyTilde": "~",
+ "KeyGrave": "`",
+ "KeyMinus": "-",
+ "KeyPlus": "+",
+ "KeyBracketLeft": "[",
+ "KeyBracketRight": "]",
+ "KeySemicolon": ";",
+ "KeyQuote": "\"",
+ "KeyComma": ",",
+ "KeyPeriod": ".",
+ "KeySlash": "/",
+ "KeyBackSlash": "\\",
+ "KeyUnbound": "Unbound",
+ "GamepadLeftStick": "L Stick Button",
+ "GamepadRightStick": "R Stick Button",
+ "GamepadLeftShoulder": "Left Shoulder",
+ "GamepadRightShoulder": "Right Shoulder",
+ "GamepadLeftTrigger": "Left Trigger",
+ "GamepadRightTrigger": "Right Trigger",
+ "GamepadDpadUp": "Up",
+ "GamepadDpadDown": "Down",
+ "GamepadDpadLeft": "Left",
+ "GamepadDpadRight": "Right",
+ "GamepadMinus": "-",
+ "GamepadPlus": "+",
+ "GamepadGuide": "Guide",
+ "GamepadMisc1": "Misc",
+ "GamepadPaddle1": "Paddle 1",
+ "GamepadPaddle2": "Paddle 2",
+ "GamepadPaddle3": "Paddle 3",
+ "GamepadPaddle4": "Paddle 4",
+ "GamepadTouchpad": "Touchpad",
+ "GamepadSingleLeftTrigger0": "Left Trigger 0",
+ "GamepadSingleRightTrigger0": "Right Trigger 0",
+ "GamepadSingleLeftTrigger1": "Left Trigger 1",
+ "GamepadSingleRightTrigger1": "Right Trigger 1",
+ "StickLeft": "Left Stick",
+ "StickRight": "Right Stick",
"UserProfilesSelectedUserProfile": "Wybrany profil użytkownika:",
"UserProfilesSaveProfileName": "Zapisz nazwę profilu",
"UserProfilesChangeProfileImage": "Zmień obrazek profilu",
@@ -341,7 +447,7 @@
"DialogThemeRestartMessage": "Motyw został zapisany. Aby zastosować motyw, konieczne jest ponowne uruchomienie.",
"DialogThemeRestartSubMessage": "Czy chcesz uruchomić ponownie?",
"DialogFirmwareInstallEmbeddedMessage": "Czy chcesz zainstalować firmware wbudowany w tę grę? (Firmware {0})",
- "DialogFirmwareInstallEmbeddedSuccessMessage": "No installed firmware was found but Ryujinx was able to install firmware {0} from the provided game.\nThe emulator will now start.",
+ "DialogFirmwareInstallEmbeddedSuccessMessage": "Nie znaleziono zainstalowanego oprogramowania, ale Ryujinx był w stanie zainstalować oprogramowanie {0} z dostarczonej gry.\n\nEmulator uruchomi się teraz.",
"DialogFirmwareNoFirmwareInstalledMessage": "Brak Zainstalowanego Firmware'u",
"DialogFirmwareInstalledMessage": "Firmware {0} został zainstalowany",
"DialogInstallFileTypesSuccessMessage": "Pomyślnie zainstalowano typy plików!",
@@ -597,6 +703,7 @@
"UserProfileWindowTitle": "Menedżer Profili Użytkowników",
"CheatWindowTitle": "Menedżer Kodów",
"DlcWindowTitle": "Menedżer Zawartości do Pobrania",
+ "ModWindowTitle": "Zarządzaj modami dla {0} ({1})",
"UpdateWindowTitle": "Menedżer Aktualizacji Tytułu",
"CheatWindowHeading": "Kody Dostępne dla {0} [{1}]",
"BuildId": "Identyfikator wersji:",
@@ -648,11 +755,11 @@
"GraphicsAALabel": "Antyaliasing:",
"GraphicsScalingFilterLabel": "Filtr skalowania:",
"GraphicsScalingFilterTooltip": "Choose the scaling filter that will be applied when using resolution scale.\n\nBilinear works well for 3D games and is a safe default option.\n\nNearest is recommended for pixel art games.\n\nFSR 1.0 is merely a sharpening filter, not recommended for use with FXAA or SMAA.\n\nThis option can be changed while a game is running by clicking \"Apply\" below; you can simply move the settings window aside and experiment until you find your preferred look for a game.\n\nLeave on BILINEAR if unsure.",
- "GraphicsScalingFilterBilinear": "Bilinear",
- "GraphicsScalingFilterNearest": "Nearest",
+ "GraphicsScalingFilterBilinear": "Dwuliniowe",
+ "GraphicsScalingFilterNearest": "Najbliższe",
"GraphicsScalingFilterFsr": "FSR",
"GraphicsScalingFilterLevelLabel": "Poziom",
- "GraphicsScalingFilterLevelTooltip": "Set FSR 1.0 sharpening level. Higher is sharper.",
+ "GraphicsScalingFilterLevelTooltip": "Ustaw poziom ostrzeżenia FSR 1.0. Wyższy jest ostrzejszy.",
"SmaaLow": "SMAA Niskie",
"SmaaMedium": "SMAA Średnie",
"SmaaHigh": "SMAA Wysokie",
@@ -660,7 +767,7 @@
"UserEditorTitle": "Edytuj użytkownika",
"UserEditorTitleCreate": "Utwórz użytkownika",
"SettingsTabNetworkInterface": "Interfejs sieci:",
- "NetworkInterfaceTooltip": "The network interface used for LAN/LDN features.\n\nIn conjunction with a VPN or XLink Kai and a game with LAN support, can be used to spoof a same-network connection over the Internet.\n\nLeave on DEFAULT if unsure.",
+ "NetworkInterfaceTooltip": "Interfejs sieciowy używany dla funkcji LAN/LDN.\n\nw połączeniu z VPN lub XLink Kai i grą z obsługą sieci LAN, może być użyty do spoofowania połączenia z tą samą siecią przez Internet.\n\nZostaw DOMYŚLNE, jeśli nie ma pewności.",
"NetworkInterfaceDefault": "Domyślny",
"PackagingShaders": "Pakuje Shadery ",
"AboutChangelogButton": "Zobacz listę zmian na GitHubie",
@@ -668,6 +775,6 @@
"SettingsTabNetworkMultiplayer": "Gra Wieloosobowa",
"MultiplayerMode": "Tryb:",
"MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.",
- "MultiplayerModeDisabled": "Disabled",
+ "MultiplayerModeDisabled": "Wyłączone",
"MultiplayerModeLdnMitm": "ldn_mitm"
}
diff --git a/src/Ryujinx/Assets/Locales/pt_BR.json b/src/Ryujinx/Assets/Locales/pt_BR.json
index fb48bd593..a8c244b65 100644
--- a/src/Ryujinx/Assets/Locales/pt_BR.json
+++ b/src/Ryujinx/Assets/Locales/pt_BR.json
@@ -30,6 +30,10 @@
"MenuBarToolsManageFileTypes": "Gerenciar tipos de arquivo",
"MenuBarToolsInstallFileTypes": "Instalar tipos de arquivo",
"MenuBarToolsUninstallFileTypes": "Desinstalar tipos de arquivos",
+ "MenuBarView": "_View",
+ "MenuBarViewWindow": "Window Size",
+ "MenuBarViewWindow720": "720p",
+ "MenuBarViewWindow1080": "1080p",
"MenuBarHelp": "_Ajuda",
"MenuBarHelpCheckForUpdates": "_Verificar se há atualizações",
"MenuBarHelpAbout": "_Sobre",
@@ -92,6 +96,7 @@
"SettingsTabGeneralEnableDiscordRichPresence": "Habilitar Rich Presence do Discord",
"SettingsTabGeneralCheckUpdatesOnLaunch": "Verificar se há atualizações ao iniciar",
"SettingsTabGeneralShowConfirmExitDialog": "Exibir diálogo de confirmação ao sair",
+ "SettingsTabGeneralRememberWindowState": "Lembrar tamanho/posição da Janela",
"SettingsTabGeneralHideCursor": "Esconder o cursor do mouse:",
"SettingsTabGeneralHideCursorNever": "Nunca",
"SettingsTabGeneralHideCursorOnIdle": "Esconder o cursor quando ocioso",
@@ -266,6 +271,107 @@
"ControllerSettingsMotionGyroDeadzone": "Zona morta do giroscópio:",
"ControllerSettingsSave": "Salvar",
"ControllerSettingsClose": "Fechar",
+ "KeyUnknown": "Unknown",
+ "KeyShiftLeft": "Shift Left",
+ "KeyShiftRight": "Shift Right",
+ "KeyControlLeft": "Ctrl Left",
+ "KeyMacControlLeft": "⌃ Left",
+ "KeyControlRight": "Ctrl Right",
+ "KeyMacControlRight": "⌃ Right",
+ "KeyAltLeft": "Alt Left",
+ "KeyMacAltLeft": "⌥ Left",
+ "KeyAltRight": "Alt Right",
+ "KeyMacAltRight": "⌥ Right",
+ "KeyWinLeft": "⊞ Left",
+ "KeyMacWinLeft": "⌘ Left",
+ "KeyWinRight": "⊞ Right",
+ "KeyMacWinRight": "⌘ Right",
+ "KeyMenu": "Menu",
+ "KeyUp": "Up",
+ "KeyDown": "Down",
+ "KeyLeft": "Left",
+ "KeyRight": "Right",
+ "KeyEnter": "Enter",
+ "KeyEscape": "Escape",
+ "KeySpace": "Space",
+ "KeyTab": "Tab",
+ "KeyBackSpace": "Backspace",
+ "KeyInsert": "Insert",
+ "KeyDelete": "Delete",
+ "KeyPageUp": "Page Up",
+ "KeyPageDown": "Page Down",
+ "KeyHome": "Home",
+ "KeyEnd": "End",
+ "KeyCapsLock": "Caps Lock",
+ "KeyScrollLock": "Scroll Lock",
+ "KeyPrintScreen": "Print Screen",
+ "KeyPause": "Pause",
+ "KeyNumLock": "Num Lock",
+ "KeyClear": "Clear",
+ "KeyKeypad0": "Keypad 0",
+ "KeyKeypad1": "Keypad 1",
+ "KeyKeypad2": "Keypad 2",
+ "KeyKeypad3": "Keypad 3",
+ "KeyKeypad4": "Keypad 4",
+ "KeyKeypad5": "Keypad 5",
+ "KeyKeypad6": "Keypad 6",
+ "KeyKeypad7": "Keypad 7",
+ "KeyKeypad8": "Keypad 8",
+ "KeyKeypad9": "Keypad 9",
+ "KeyKeypadDivide": "Keypad Divide",
+ "KeyKeypadMultiply": "Keypad Multiply",
+ "KeyKeypadSubtract": "Keypad Subtract",
+ "KeyKeypadAdd": "Keypad Add",
+ "KeyKeypadDecimal": "Keypad Decimal",
+ "KeyKeypadEnter": "Keypad Enter",
+ "KeyNumber0": "0",
+ "KeyNumber1": "1",
+ "KeyNumber2": "2",
+ "KeyNumber3": "3",
+ "KeyNumber4": "4",
+ "KeyNumber5": "5",
+ "KeyNumber6": "6",
+ "KeyNumber7": "7",
+ "KeyNumber8": "8",
+ "KeyNumber9": "9",
+ "KeyTilde": "~",
+ "KeyGrave": "`",
+ "KeyMinus": "-",
+ "KeyPlus": "+",
+ "KeyBracketLeft": "[",
+ "KeyBracketRight": "]",
+ "KeySemicolon": ";",
+ "KeyQuote": "\"",
+ "KeyComma": ",",
+ "KeyPeriod": ".",
+ "KeySlash": "/",
+ "KeyBackSlash": "\\",
+ "KeyUnbound": "Unbound",
+ "GamepadLeftStick": "L Stick Button",
+ "GamepadRightStick": "R Stick Button",
+ "GamepadLeftShoulder": "Left Shoulder",
+ "GamepadRightShoulder": "Right Shoulder",
+ "GamepadLeftTrigger": "Left Trigger",
+ "GamepadRightTrigger": "Right Trigger",
+ "GamepadDpadUp": "Up",
+ "GamepadDpadDown": "Down",
+ "GamepadDpadLeft": "Left",
+ "GamepadDpadRight": "Right",
+ "GamepadMinus": "-",
+ "GamepadPlus": "+",
+ "GamepadGuide": "Guide",
+ "GamepadMisc1": "Misc",
+ "GamepadPaddle1": "Paddle 1",
+ "GamepadPaddle2": "Paddle 2",
+ "GamepadPaddle3": "Paddle 3",
+ "GamepadPaddle4": "Paddle 4",
+ "GamepadTouchpad": "Touchpad",
+ "GamepadSingleLeftTrigger0": "Left Trigger 0",
+ "GamepadSingleRightTrigger0": "Right Trigger 0",
+ "GamepadSingleLeftTrigger1": "Left Trigger 1",
+ "GamepadSingleRightTrigger1": "Right Trigger 1",
+ "StickLeft": "Left Stick",
+ "StickRight": "Right Stick",
"UserProfilesSelectedUserProfile": "Perfil de usuário selecionado:",
"UserProfilesSaveProfileName": "Salvar nome de perfil",
"UserProfilesChangeProfileImage": "Mudar imagem de perfil",
@@ -597,6 +703,7 @@
"UserProfileWindowTitle": "Gerenciador de perfis de usuário",
"CheatWindowTitle": "Gerenciador de Cheats",
"DlcWindowTitle": "Gerenciador de DLC",
+ "ModWindowTitle": "Manage Mods for {0} ({1})",
"UpdateWindowTitle": "Gerenciador de atualizações",
"CheatWindowHeading": "Cheats disponíveis para {0} [{1}]",
"BuildId": "ID da Build",
diff --git a/src/Ryujinx/Assets/Locales/ru_RU.json b/src/Ryujinx/Assets/Locales/ru_RU.json
index 284b8e2b4..75fd4fe12 100644
--- a/src/Ryujinx/Assets/Locales/ru_RU.json
+++ b/src/Ryujinx/Assets/Locales/ru_RU.json
@@ -1,9 +1,9 @@
{
"Language": "Русский (RU)",
"MenuBarFileOpenApplet": "Открыть апплет",
- "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Открыть апплет Mii Editor в автономном режиме",
+ "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Открывает апплет Mii Editor в автономном режиме",
"SettingsTabInputDirectMouseAccess": "Прямой ввод мыши",
- "SettingsTabSystemMemoryManagerMode": "Режим диспетчера памяти:",
+ "SettingsTabSystemMemoryManagerMode": "Режим менеджера памяти:",
"SettingsTabSystemMemoryManagerModeSoftware": "Программное обеспечение",
"SettingsTabSystemMemoryManagerModeHost": "Хост (быстро)",
"SettingsTabSystemMemoryManagerModeHostUnchecked": "Хост не установлен (самый быстрый, небезопасный)",
@@ -30,6 +30,10 @@
"MenuBarToolsManageFileTypes": "Управление типами файлов",
"MenuBarToolsInstallFileTypes": "Установить типы файлов",
"MenuBarToolsUninstallFileTypes": "Удалить типы файлов",
+ "MenuBarView": "_Вид",
+ "MenuBarViewWindow": "Размер окна",
+ "MenuBarViewWindow720": "720p",
+ "MenuBarViewWindow1080": "1080p",
"MenuBarHelp": "_Помощь",
"MenuBarHelpCheckForUpdates": "Проверить наличие обновлений",
"MenuBarHelpAbout": "О программе",
@@ -40,7 +44,7 @@
"GameListHeaderDeveloper": "Разработчик",
"GameListHeaderVersion": "Версия",
"GameListHeaderTimePlayed": "Время в игре",
- "GameListHeaderLastPlayed": "Последняя игра",
+ "GameListHeaderLastPlayed": "Последний запуск",
"GameListHeaderFileExtension": "Расширение файла",
"GameListHeaderFileSize": "Размер файла",
"GameListHeaderPath": "Путь",
@@ -80,7 +84,7 @@
"StatusBarGamesLoaded": "{0}/{1} игр загружено",
"StatusBarSystemVersion": "Версия прошивки: {0}",
"LinuxVmMaxMapCountDialogTitle": "Обнаружен низкий лимит разметки памяти",
- "LinuxVmMaxMapCountDialogTextPrimary": "Вы хотите увеличить значение vm.max_map_count до {0}",
+ "LinuxVmMaxMapCountDialogTextPrimary": "Хотите увеличить значение vm.max_map_count до {0}",
"LinuxVmMaxMapCountDialogTextSecondary": "Некоторые игры могут создавать большую разметку памяти, чем разрешено на данный момент по умолчанию. Ryujinx вылетит при превышении этого лимита.",
"LinuxVmMaxMapCountDialogButtonUntilRestart": "Да, до следующего перезапуска",
"LinuxVmMaxMapCountDialogButtonPersistent": "Да, постоянно",
@@ -89,12 +93,13 @@
"Settings": "Параметры",
"SettingsTabGeneral": "Интерфейс",
"SettingsTabGeneralGeneral": "Общее",
- "SettingsTabGeneralEnableDiscordRichPresence": "Включить статус активности в Discord",
+ "SettingsTabGeneralEnableDiscordRichPresence": "Статус активности в Discord",
"SettingsTabGeneralCheckUpdatesOnLaunch": "Проверять наличие обновлений при запуске",
"SettingsTabGeneralShowConfirmExitDialog": "Подтверждать выход из приложения",
+ "SettingsTabGeneralRememberWindowState": "Запомнить размер/положение окна",
"SettingsTabGeneralHideCursor": "Скрывать курсор",
"SettingsTabGeneralHideCursorNever": "Никогда",
- "SettingsTabGeneralHideCursorOnIdle": "В режиме ожидания",
+ "SettingsTabGeneralHideCursorOnIdle": "В простое",
"SettingsTabGeneralHideCursorAlways": "Всегда",
"SettingsTabGeneralGameDirectories": "Папки с играми",
"SettingsTabGeneralAdd": "Добавить",
@@ -125,13 +130,13 @@
"SettingsTabSystemSystemLanguageBritishEnglish": "Английский (Британия)",
"SettingsTabSystemSystemLanguageCanadianFrench": "Французский (Канада)",
"SettingsTabSystemSystemLanguageLatinAmericanSpanish": "Испанский (Латинская Америка)",
- "SettingsTabSystemSystemLanguageSimplifiedChinese": "Китайский упрощённый",
- "SettingsTabSystemSystemLanguageTraditionalChinese": "Китайский традиционный",
+ "SettingsTabSystemSystemLanguageSimplifiedChinese": "Китайский (упрощённый)",
+ "SettingsTabSystemSystemLanguageTraditionalChinese": "Китайский (традиционный)",
"SettingsTabSystemSystemTimeZone": "Часовой пояс прошивки:",
- "SettingsTabSystemSystemTime": "Время в прошивке:",
- "SettingsTabSystemEnableVsync": "Включить вертикальную синхронизацию",
- "SettingsTabSystemEnablePptc": "Включить PPTC (Profiled Persistent Translation Cache)",
- "SettingsTabSystemEnableFsIntegrityChecks": "Включить проверку целостности файловой системы",
+ "SettingsTabSystemSystemTime": "Системное время в прошивке:",
+ "SettingsTabSystemEnableVsync": "Вертикальная синхронизация",
+ "SettingsTabSystemEnablePptc": "Использовать PPTC (Profiled Persistent Translation Cache)",
+ "SettingsTabSystemEnableFsIntegrityChecks": "Проверка целостности файловой системы",
"SettingsTabSystemAudioBackend": "Аудио бэкенд:",
"SettingsTabSystemAudioBackendDummy": "Без звука",
"SettingsTabSystemAudioBackendOpenAL": "OpenAL",
@@ -143,7 +148,7 @@
"SettingsTabSystemIgnoreMissingServices": "Игнорировать отсутствующие службы",
"SettingsTabGraphics": "Графика",
"SettingsTabGraphicsAPI": "Графические API",
- "SettingsTabGraphicsEnableShaderCache": "Включить кэш шейдеров",
+ "SettingsTabGraphicsEnableShaderCache": "Кэшировать шейдеры",
"SettingsTabGraphicsAnisotropicFiltering": "Анизотропная фильтрация:",
"SettingsTabGraphicsAnisotropicFilteringAuto": "Автоматически",
"SettingsTabGraphicsAnisotropicFiltering2x": "2x",
@@ -164,7 +169,7 @@
"SettingsTabGraphicsAspectRatio32x9": "32:9",
"SettingsTabGraphicsAspectRatioStretch": "Растянуть до размеров окна",
"SettingsTabGraphicsDeveloperOptions": "Параметры разработчика",
- "SettingsTabGraphicsShaderDumpPath": "Путь дампа графического шейдера:",
+ "SettingsTabGraphicsShaderDumpPath": "Путь дампа графических шейдеров",
"SettingsTabLogging": "Журналирование",
"SettingsTabLoggingLogging": "Журналирование",
"SettingsTabLoggingEnableLoggingToFile": "Включить запись в файл",
@@ -266,6 +271,107 @@
"ControllerSettingsMotionGyroDeadzone": "Мертвая зона гироскопа:",
"ControllerSettingsSave": "Сохранить",
"ControllerSettingsClose": "Закрыть",
+ "KeyUnknown": "Неизвестно",
+ "KeyShiftLeft": "Левый Shift",
+ "KeyShiftRight": "Правый Shift",
+ "KeyControlLeft": "Левый Ctrl",
+ "KeyMacControlLeft": "Левый ⌃",
+ "KeyControlRight": "Правый Ctrl",
+ "KeyMacControlRight": "Правый ⌃",
+ "KeyAltLeft": "Левый Alt",
+ "KeyMacAltLeft": "Левый ⌥",
+ "KeyAltRight": "Правый Alt",
+ "KeyMacAltRight": "Правый ⌥",
+ "KeyWinLeft": "Левый ⊞",
+ "KeyMacWinLeft": "Левый ⌘",
+ "KeyWinRight": "Правый ⊞",
+ "KeyMacWinRight": "Правый ⌘",
+ "KeyMenu": "Меню",
+ "KeyUp": "Вверх",
+ "KeyDown": "Вниз",
+ "KeyLeft": "Влево",
+ "KeyRight": "Вправо",
+ "KeyEnter": "Enter",
+ "KeyEscape": "Escape",
+ "KeySpace": "Пробел",
+ "KeyTab": "Tab",
+ "KeyBackSpace": "Backspace",
+ "KeyInsert": "Insert",
+ "KeyDelete": "Delete",
+ "KeyPageUp": "Page Up",
+ "KeyPageDown": "Page Down",
+ "KeyHome": "Home",
+ "KeyEnd": "End",
+ "KeyCapsLock": "Caps Lock",
+ "KeyScrollLock": "Scroll Lock",
+ "KeyPrintScreen": "Print Screen",
+ "KeyPause": "Pause",
+ "KeyNumLock": "Num Lock",
+ "KeyClear": "Очистить",
+ "KeyKeypad0": "Блок цифр 0",
+ "KeyKeypad1": "Блок цифр 1",
+ "KeyKeypad2": "Блок цифр 2",
+ "KeyKeypad3": "Блок цифр 3",
+ "KeyKeypad4": "Блок цифр 4",
+ "KeyKeypad5": "Блок цифр 5",
+ "KeyKeypad6": "Блок цифр 6",
+ "KeyKeypad7": "Блок цифр 7",
+ "KeyKeypad8": "Блок цифр 8",
+ "KeyKeypad9": "Блок цифр 9",
+ "KeyKeypadDivide": "/ (блок цифр)",
+ "KeyKeypadMultiply": "* (блок цифр)",
+ "KeyKeypadSubtract": "- (блок цифр)",
+ "KeyKeypadAdd": "+ (блок цифр)",
+ "KeyKeypadDecimal": ". (блок цифр)",
+ "KeyKeypadEnter": "Enter (блок цифр)",
+ "KeyNumber0": "0",
+ "KeyNumber1": "1",
+ "KeyNumber2": "2",
+ "KeyNumber3": "3",
+ "KeyNumber4": "4",
+ "KeyNumber5": "5",
+ "KeyNumber6": "6",
+ "KeyNumber7": "7",
+ "KeyNumber8": "8",
+ "KeyNumber9": "9",
+ "KeyTilde": "~",
+ "KeyGrave": "`",
+ "KeyMinus": "-",
+ "KeyPlus": "+",
+ "KeyBracketLeft": "[",
+ "KeyBracketRight": "]",
+ "KeySemicolon": ";",
+ "KeyQuote": "\"",
+ "KeyComma": ",",
+ "KeyPeriod": ".",
+ "KeySlash": "/",
+ "KeyBackSlash": "\\",
+ "KeyUnbound": "Не привязано",
+ "GamepadLeftStick": "Кнопка лев. стика",
+ "GamepadRightStick": "Кнопка пр. стика",
+ "GamepadLeftShoulder": "Левый бампер",
+ "GamepadRightShoulder": "Правый бампер",
+ "GamepadLeftTrigger": "Левый триггер",
+ "GamepadRightTrigger": "Правый триггер",
+ "GamepadDpadUp": "Вверх",
+ "GamepadDpadDown": "Вниз",
+ "GamepadDpadLeft": "Влево",
+ "GamepadDpadRight": "Вправо",
+ "GamepadMinus": "-",
+ "GamepadPlus": "+",
+ "GamepadGuide": "Кнопка Xbox",
+ "GamepadMisc1": "Прочее",
+ "GamepadPaddle1": "Доп.кнопка 1",
+ "GamepadPaddle2": "Доп.кнопка 2",
+ "GamepadPaddle3": "Доп.кнопка 3",
+ "GamepadPaddle4": "Доп.кнопка 4",
+ "GamepadTouchpad": "Тачпад",
+ "GamepadSingleLeftTrigger0": "Левый триггер 0",
+ "GamepadSingleRightTrigger0": "Правый триггер 0",
+ "GamepadSingleLeftTrigger1": "Левый триггер 1",
+ "GamepadSingleRightTrigger1": "Правый триггер 1",
+ "StickLeft": "Левый стик",
+ "StickRight": "Правый стик",
"UserProfilesSelectedUserProfile": "Выбранный пользовательский профиль:",
"UserProfilesSaveProfileName": "Сохранить пользовательский профиль",
"UserProfilesChangeProfileImage": "Изменить аватар",
@@ -273,17 +379,17 @@
"UserProfilesAddNewProfile": "Добавить новый профиль",
"UserProfilesDelete": "Удалить",
"UserProfilesClose": "Закрыть",
- "ProfileNameSelectionWatermark": "Выберите никнейм",
+ "ProfileNameSelectionWatermark": "Укажите никнейм",
"ProfileImageSelectionTitle": "Выбор изображения профиля",
- "ProfileImageSelectionHeader": "Выберите аватар",
+ "ProfileImageSelectionHeader": "Выбор аватара",
"ProfileImageSelectionNote": "Вы можете импортировать собственное изображение или выбрать аватар из системной прошивки.",
"ProfileImageSelectionImportImage": "Импорт изображения",
"ProfileImageSelectionSelectAvatar": "Встроенные аватары",
"InputDialogTitle": "Диалоговое окно ввода",
"InputDialogOk": "ОК",
"InputDialogCancel": "Отмена",
- "InputDialogAddNewProfileTitle": "Выберите имя профиля",
- "InputDialogAddNewProfileHeader": "Пожалуйста, введите имя профиля",
+ "InputDialogAddNewProfileTitle": "Выберите никнейм",
+ "InputDialogAddNewProfileHeader": "Пожалуйста, введите никнейм",
"InputDialogAddNewProfileSubtext": "(Максимальная длина: {0})",
"AvatarChoose": "Выбрать аватар",
"AvatarSetBackgroundColor": "Установить цвет фона",
@@ -312,7 +418,7 @@
"DialogWarningTitle": "Ryujinx - Предупреждение",
"DialogExitTitle": "Ryujinx - Выход",
"DialogErrorMessage": "Ryujinx обнаружил ошибку",
- "DialogExitMessage": "Вы уверены, что хотите закрыть Ryujinx?",
+ "DialogExitMessage": "Вы уверены, что хотите выйти из Ryujinx?",
"DialogExitSubMessage": "Все несохраненные данные будут потеряны",
"DialogMessageCreateSaveErrorMessage": "Произошла ошибка при создании указанных данных сохранения: {0}",
"DialogMessageFindSaveErrorMessage": "Произошла ошибка при поиске указанных данных сохранения: {0}",
@@ -324,7 +430,7 @@
"DialogNcaExtractionSuccessMessage": "Извлечение завершено успешно.",
"DialogUpdaterConvertFailedMessage": "Не удалось преобразовать текущую версию Ryujinx.",
"DialogUpdaterCancelUpdateMessage": "Отмена обновления...",
- "DialogUpdaterAlreadyOnLatestVersionMessage": "Вы уже используете самую последнюю версию Ryujinx",
+ "DialogUpdaterAlreadyOnLatestVersionMessage": "Вы используете самую последнюю версию Ryujinx",
"DialogUpdaterFailedToGetVersionMessage": "Произошла ошибка при попытке получить информацию о выпуске от GitHub Release. Это может быть вызвано тем, что в данный момент в GitHub Actions компилируется новый релиз. Повторите попытку позже.",
"DialogUpdaterConvertFailedGithubMessage": "Не удалось преобразовать полученную версию Ryujinx из Github Release.",
"DialogUpdaterDownloadingMessage": "Загрузка обновления...",
@@ -332,14 +438,14 @@
"DialogUpdaterRenamingMessage": "Переименование обновления...",
"DialogUpdaterAddingFilesMessage": "Добавление нового обновления...",
"DialogUpdaterCompleteMessage": "Обновление завершено",
- "DialogUpdaterRestartMessage": "Вы хотите перезапустить Ryujinx сейчас?",
+ "DialogUpdaterRestartMessage": "Перезапустить Ryujinx?",
"DialogUpdaterNoInternetMessage": "Вы не подключены к интернету",
"DialogUpdaterNoInternetSubMessage": "Убедитесь, что у вас работает подключение к интернету",
"DialogUpdaterDirtyBuildMessage": "Вы не можете обновлять Dirty Build",
"DialogUpdaterDirtyBuildSubMessage": "Загрузите Ryujinx по адресу https://ryujinx.org/ если вам нужна поддерживаемая версия.",
"DialogRestartRequiredMessage": "Требуется перезагрузка",
"DialogThemeRestartMessage": "Тема сохранена. Для применения темы требуется перезапуск.",
- "DialogThemeRestartSubMessage": "Вы хотите перезапустить?",
+ "DialogThemeRestartSubMessage": "Хотите перезапустить",
"DialogFirmwareInstallEmbeddedMessage": "Хотите установить прошивку, встроенную в эту игру? (Прошивка {0})",
"DialogFirmwareInstallEmbeddedSuccessMessage": "Установленная прошивка не была найдена, но Ryujinx удалось установить прошивку {0} из предоставленной игры.\nТеперь эмулятор запустится.",
"DialogFirmwareNoFirmwareInstalledMessage": "Прошивка не установлена",
@@ -348,7 +454,7 @@
"DialogInstallFileTypesErrorMessage": "Не удалось установить типы файлов.",
"DialogUninstallFileTypesSuccessMessage": "Типы файлов успешно удалены",
"DialogUninstallFileTypesErrorMessage": "Не удалось удалить типы файлов.",
- "DialogOpenSettingsWindowLabel": "Открыть окно параметров",
+ "DialogOpenSettingsWindowLabel": "Открывает окно параметров",
"DialogControllerAppletTitle": "Апплет контроллера",
"DialogMessageDialogErrorExceptionMessage": "Ошибка отображения сообщения: {0}",
"DialogSoftwareKeyboardErrorExceptionMessage": "Ошибка отображения программной клавиатуры: {0}",
@@ -364,7 +470,7 @@
"DialogProfileDeleteProfileTitle": "Удаление профиля",
"DialogProfileDeleteProfileMessage": "Это действие необратимо. Вы уверены, что хотите продолжить?",
"DialogWarning": "Внимание",
- "DialogPPTCDeletionMessage": "Вы собираетесь удалить кэш PPTC для:\n\n{0}\n\nВы уверены, что хотите продолжить?",
+ "DialogPPTCDeletionMessage": "Вы собираетесь перестроить кэш PPTC при следующем запуске для:\n\n{0}\n\nВы уверены, что хотите продолжить?",
"DialogPPTCDeletionErrorMessage": "Ошибка очистки кэша PPTC в {0}: {1}",
"DialogShaderDeletionMessage": "Вы собираетесь удалить кэш шейдеров для:\n\n{0}\n\nВы уверены, что хотите продолжить?",
"DialogShaderDeletionErrorMessage": "Ошибка очистки кэша шейдеров в {0}: {1}",
@@ -378,10 +484,10 @@
"DialogFirmwareInstallerFirmwareInstallWaitMessage": "Установка прошивки...",
"DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Прошивка версии {0} успешно установлена.",
"DialogUserProfileDeletionWarningMessage": "Если выбранный профиль будет удален, другие профили не будут открываться.",
- "DialogUserProfileDeletionConfirmMessage": "Вы хотите удалить выбранный профиль",
+ "DialogUserProfileDeletionConfirmMessage": "Удалить выбранный профиль?",
"DialogUserProfileUnsavedChangesTitle": "Внимание - Несохраненные изменения",
- "DialogUserProfileUnsavedChangesMessage": "Вы внесли изменения в этот профиль пользователя которые не были сохранены.",
- "DialogUserProfileUnsavedChangesSubMessage": "Вы хотите отменить изменения?",
+ "DialogUserProfileUnsavedChangesMessage": "В эту учетную запись внесены изменения, которые не были сохранены.",
+ "DialogUserProfileUnsavedChangesSubMessage": "Отменить изменения?",
"DialogControllerSettingsModifiedConfirmMessage": "Текущие настройки управления обновлены.",
"DialogControllerSettingsModifiedConfirmSubMessage": "Сохранить?",
"DialogLoadFileErrorMessage": "{0}. Файл с ошибкой: {1}",
@@ -444,40 +550,40 @@
"OrderDescending": "По убыванию",
"SettingsTabGraphicsFeatures": "Функции",
"ErrorWindowTitle": "Окно ошибки",
- "ToggleDiscordTooltip": "Включает или отключает отображение в Discord статуса \"Сейчас играет\"",
- "AddGameDirBoxTooltip": "Введите папку игры для добавления в список",
+ "ToggleDiscordTooltip": "Включает или отключает отображение статуса \"Играет в игру\" в Discord",
+ "AddGameDirBoxTooltip": "Введите путь к папке с играми для добавления ее в список выше",
"AddGameDirTooltip": "Добавить папку с играми в список",
"RemoveGameDirTooltip": "Удалить выбранную папку игры",
"CustomThemeCheckTooltip": "Включить или отключить пользовательские темы",
"CustomThemePathTooltip": "Путь к пользовательской теме для интерфейса",
"CustomThemeBrowseTooltip": "Просмотр пользовательской темы интерфейса",
- "DockModeToggleTooltip": "\"Стационарный\" режим запускает эмулятор, как если бы Nintendo Switch находилась в доке, что улучшает графику и разрешение в большинстве игр. И наоборот, при отключении этого режима эмулятор будет запускать игры в \"Портативном\" режиме, снижая качество графики.\n\nНастройте управление для Игрока 1 если планируете использовать в \"Стационарном\" режиме; настройте портативное управление если планируете использовать эмулятор в \"Портативном\" режиме.\n\nОставьте включенным если не уверены.",
+ "DockModeToggleTooltip": "\"Стационарный\" режим запускает эмулятор, как если бы Nintendo Switch находилась в доке, что улучшает графику и разрешение в большинстве игр. И наоборот, при отключении этого режима эмулятор будет запускать игры в \"Портативном\" режиме, снижая качество графики.\n\nНастройте управление для Игрока 1 если планируете использовать в \"Стационарном\" режиме; настройте портативное управление если планируете использовать эмулятор в \"Портативном\" режиме.\n\nРекомендуется оставить включенным.",
"DirectKeyboardTooltip": "Поддержка прямого ввода с клавиатуры (HID). Предоставляет игре прямой доступ к клавиатуре в качестве устройства ввода текста.\nРаботает только с играми, которые изначально поддерживают использование клавиатуры с Switch.\nРекомендуется оставить выключенным.",
"DirectMouseTooltip": "Поддержка прямого ввода мыши (HID). Предоставляет игре прямой доступ к мыши в качестве указывающего устройства.\nРаботает только с играми, которые изначально поддерживают использование мыши совместно с железом Switch.\nРекомендуется оставить выключенным.",
- "RegionTooltip": "Изменение региона прошивки",
- "LanguageTooltip": "Изменение языка прошивки",
- "TimezoneTooltip": "Изменение часового пояса прошивки",
- "TimeTooltip": "Изменение системного времени",
+ "RegionTooltip": "Сменяет регион прошивки",
+ "LanguageTooltip": "Меняет язык прошивки",
+ "TimezoneTooltip": "Меняет часовой пояс прошивки",
+ "TimeTooltip": "Меняет системное время прошивки",
"VSyncToggleTooltip": "Эмуляция вертикальной синхронизации консоли, которая ограничивает количество кадров в секунду в большинстве игр; отключение может привести к тому, что игры будут запущены с более высокой частотой кадров, но загрузка игры может занять больше времени, либо игра не запустится вообще.\n\nМожно включать и выключать эту настройку непосредственно в игре с помощью горячих клавиш (F1 по умолчанию). Если планируете отключить вертикальную синхронизацию, рекомендуем настроить горячие клавиши.\n\nРекомендуется оставить включенным.",
- "PptcToggleTooltip": "Сохранение преобразованных JIT-функций для того, чтобы не преобразовывать их по новой каждый раз при запуске игры.\n\nУменьшает статтеры и значительно ускоряет последующую загрузку игр.\n\nРекомендуется оставить включенным.",
- "FsIntegrityToggleTooltip": "Проверяет поврежденные файлы при загрузке игры и если поврежденные файлы обнаружены, отображает ошибку о поврежденном хэше в журнале.\n\nНе влияет на производительность и необходим для помощи в устранении неполадок.\n\nРекомендуется оставить включенным.",
- "AudioBackendTooltip": "Изменяет используемый аудио-бэкенд для рендера звука.\n\nSDL2 является предпочтительным, в то время как OpenAL и SoundIO используются в качестве резервных. \n\nРекомендуется использование SDL2.",
- "MemoryManagerTooltip": "Изменение разметки и доступа к гостевой памяти. Значительно влияет на производительность процессора.\n\nРекомендуется оставить \"Хост не установлен\"",
- "MemoryManagerSoftwareTooltip": "Использует таблицу страниц для преобразования адресов. Самая высокая точность, но самая низкая производительность.",
- "MemoryManagerHostTooltip": "Прямая разметка памяти в адресном пространстве хоста. Значительно более быстрая JIT-компиляция и запуск.",
- "MemoryManagerUnsafeTooltip": "Производит прямую разметку памяти, но не маскирует адрес в гостевом адресном пространстве перед получением доступа. Быстро, но небезопасно. Гостевое приложение может получить доступ к памяти из Ryujinx, поэтому в этом режиме рекомендуется запускать только те программы, которым вы доверяете.",
+ "PptcToggleTooltip": "Сохраняет скомпилированные JIT-функции для того, чтобы не преобразовывать их по новой каждый раз при запуске игры.\n\nУменьшает статтеры и значительно ускоряет последующую загрузку игр.\n\nРекомендуется оставить включенным.",
+ "FsIntegrityToggleTooltip": "Проверяет файлы при загрузке игры и если обнаружены поврежденные файлы, выводит сообщение о поврежденном хэше в журнале.\n\nНе влияет на производительность и необходим для помощи в устранении неполадок.\n\nРекомендуется оставить включенным.",
+ "AudioBackendTooltip": "Изменяет используемый аудио бэкенд для рендера звука.\n\nSDL2 является предпочтительным вариантом, в то время как OpenAL и SoundIO используются в качестве резервных.\n\nРекомендуется использование SDL2.",
+ "MemoryManagerTooltip": "Меняет разметку и доступ к гостевой памяти. Значительно влияет на производительность процессора.\n\nРекомендуется оставить \"Хост не установлен\"",
+ "MemoryManagerSoftwareTooltip": "Использует таблицу страниц для преобразования адресов. \nСамая высокая точность, но самая низкая производительность.",
+ "MemoryManagerHostTooltip": "Прямая разметка памяти в адресном пространстве хоста. \nЗначительно более быстрые запуск и компиляция JIT.",
+ "MemoryManagerUnsafeTooltip": "Производит прямую разметку памяти, но не маскирует адрес в гостевом адресном пространстве перед получением доступа. \nБыстро, но небезопасно. Гостевое приложение может получить доступ к памяти из Ryujinx, поэтому в этом режиме рекомендуется запускать только те программы, которым вы доверяете.",
"UseHypervisorTooltip": "Использует Hypervisor вместо JIT. Значительно увеличивает производительность, но может работать нестабильно.",
"DRamTooltip": "Использует альтернативный макет MemoryMode для имитации использования Nintendo Switch в режиме разработчика.\n\nПолезно только для пакетов текстур с высоким разрешением или модов добавляющих разрешение 4К. Не улучшает производительность.\n\nРекомендуется оставить выключенным.",
"IgnoreMissingServicesTooltip": "Игнорирует нереализованные сервисы Horizon в новых прошивках. Эта настройка поможет избежать вылеты при запуске определенных игр.\n\nРекомендуется оставить выключенным.",
- "GraphicsBackendThreadingTooltip": "Выполняет команды графического бэкенда на втором потоке.\n\nУскоряет компиляцию шейдеров, уменьшает статтеры и повышает производительность на драйверах GPU без поддержки многопоточности. Производительность на драйверах с многопоточностью немного выше.\n\nРекомендуется оставить Автоматически.",
- "GalThreadingTooltip": "Выполняет команды графического бэкенда на втором потоке.\n\nУскоряет компиляцию шейдеров, уменьшает статтеры и повышает производительность на драйверах GPU без поддержки многопоточности. Производительность на драйверах с многопоточностью немного выше.\n\nРекомендуется оставить в Авто.",
+ "GraphicsBackendThreadingTooltip": "Выполняет команды графического бэкенда на втором потоке.\n\nУскоряет компиляцию шейдеров, уменьшает статтеры и повышает производительность на драйверах видеоадаптера без поддержки многопоточности. Производительность на драйверах с многопоточностью немного выше.\n\nРекомендуется оставить Автоматически.",
+ "GalThreadingTooltip": "Выполняет команды графического бэкенда на втором потоке.\n\nУскоряет компиляцию шейдеров, уменьшает статтеры и повышает производительность на драйверах видеоадаптера без поддержки многопоточности. Производительность на драйверах с многопоточностью немного выше.\n\nРекомендуется оставить Автоматически.",
"ShaderCacheToggleTooltip": "Сохраняет кэш шейдеров на диске, для уменьшения статтеров при последующих запусках.\n\nРекомендуется оставить включенным.",
- "ResolutionScaleTooltip": "Увеличивает разрешение рендера игры.\n\nНекоторые игры могут не работать с этой настройкой и выглядеть смазано даже когда разрешение увеличено; Для таких игр вам может потребоваться установить моды, которые убирают сглаживание или увеличивают разрешение рендеринга. Для использования последнего, вам нужно будет выбрать опцию \"Нативное\".\n\nЭта опция может быть изменена во время игры по нажатию кнопки \"Применить\" ниже; Вы можете просто переместить окно настроек в сторону и поэкспериментировать, пока не подберете подходящие настройки для конкретной игры.\n\nИмейте в виду, что \"4x\" является излишеством.",
+ "ResolutionScaleTooltip": "Увеличивает разрешение рендера игры.\n\nНекоторые игры могут не работать с этой настройкой и выглядеть смазано даже когда разрешение увеличено. Для таких игр может потребоваться установка модов, которые убирают сглаживание или увеличивают разрешение рендеринга. \nДля использования последнего, вам нужно будет выбрать опцию \"Нативное\".\n\nЭта опция может быть изменена во время игры по нажатию кнопки \"Применить\" ниже. Вы можете просто переместить окно настроек в сторону и поэкспериментировать, пока не подберете подходящие настройки для конкретной игры.\n\nИмейте в виду, что \"4x\" является излишеством.",
"ResolutionScaleEntryTooltip": "Масштабирование разрешения с плавающей запятой, например 1,5. Неинтегральное масштабирование с большой вероятностью вызовет сбои в работе.",
- "AnisotropyTooltip": "Уровень анизотропной фильтрации. Установите значение «Авто», чтобы использовать в игре значение по умолчанию игре.",
+ "AnisotropyTooltip": "Уровень анизотропной фильтрации. \n\nУстановите значение Автоматически, чтобы использовать значение по умолчанию игры.",
"AspectRatioTooltip": "Соотношение сторон окна рендерера.\n\nИзмените эту настройку только если вы используете мод для соотношения сторон, иначе изображение будет растянуто.\n\nРекомендуется настройка 16:9.",
"ShaderDumpPathTooltip": "Путь с дампами графических шейдеров",
- "FileLogTooltip": "Включает или отключает ведение журнала в файл на диске. Не влияет на производительность.",
+ "FileLogTooltip": "Включает ведение журнала в файл на диске. Не влияет на производительность.",
"StubLogTooltip": "Включает ведение журнала-заглушки. Не влияет на производительность.",
"InfoLogTooltip": "Включает вывод сообщений информационного журнала в консоль. Не влияет на производительность.",
"WarnLogTooltip": "Включает вывод сообщений журнала предупреждений в консоль. Не влияет на производительность.",
@@ -489,42 +595,42 @@
"DeveloperOptionTooltip": "Используйте с осторожностью",
"OpenGlLogLevel": "Требует включения соответствующих уровней ведения журнала",
"DebugLogTooltip": "Выводит журнал сообщений отладки в консоли.\n\nИспользуйте только в случае просьбы разработчика, так как включение этой функции затруднит чтение журналов и ухудшит работу эмулятора.",
- "LoadApplicationFileTooltip": "Открыть файловый менеджер для выбора файла, совместимого с Nintendo Switch.",
- "LoadApplicationFolderTooltip": "Открыть файловый менеджер для выбора распакованного приложения, совместимого с Nintendo Switch.",
+ "LoadApplicationFileTooltip": "Открывает файловый менеджер для выбора файла, совместимого с Nintendo Switch.",
+ "LoadApplicationFolderTooltip": "Открывает файловый менеджер для выбора распакованного приложения, совместимого с Nintendo Switch.",
"OpenRyujinxFolderTooltip": "Открывает папку с файлами Ryujinx. ",
"OpenRyujinxLogsTooltip": "Открывает папку в которую записываются логи",
"ExitTooltip": "Выйти из Ryujinx",
- "OpenSettingsTooltip": "Открыть окно параметров",
+ "OpenSettingsTooltip": "Открывает окно параметров",
"OpenProfileManagerTooltip": "Открыть менеджер учетных записей",
"StopEmulationTooltip": "Остановка эмуляции текущей игры и возврат к списку игр",
- "CheckUpdatesTooltip": "Проверка наличия обновления Ryujinx",
- "OpenAboutTooltip": "Открыть окно «О программе»",
+ "CheckUpdatesTooltip": "Проверяет наличие обновлений для Ryujinx",
+ "OpenAboutTooltip": "Открывает окно «О программе»",
"GridSize": "Размер сетки",
- "GridSizeTooltip": "Изменение размера элементов сетки",
+ "GridSizeTooltip": "Меняет размер сетки элементов",
"SettingsTabSystemSystemLanguageBrazilianPortuguese": "Португальский язык (Бразилия)",
"AboutRyujinxContributorsButtonHeader": "Посмотреть всех участников",
"SettingsTabSystemAudioVolume": "Громкость: ",
"AudioVolumeTooltip": "Изменяет громкость звука",
- "SettingsTabSystemEnableInternetAccess": "Включить гостевой доступ в Интернет/сетевой режим",
+ "SettingsTabSystemEnableInternetAccess": "Гостевой доступ в интернет/сетевой режим",
"EnableInternetAccessTooltip": "Позволяет эмулированному приложению подключаться к Интернету.\n\nПри включении этой функции игры с возможностью сетевой игры могут подключаться друг к другу, если все эмуляторы (или реальные консоли) подключены к одной и той же точке доступа.\n\nНЕ разрешает подключение к серверам Nintendo. Может вызвать сбой в некоторых играх, которые пытаются подключиться к Интернету.\n\nРекомендутеся оставить выключенным.",
- "GameListContextMenuManageCheatToolTip": "Управление читами",
+ "GameListContextMenuManageCheatToolTip": "Открывает окно управления читами",
"GameListContextMenuManageCheat": "Управление читами",
- "GameListContextMenuManageModToolTip": "Управление модами",
+ "GameListContextMenuManageModToolTip": "Открывает окно управления модами",
"GameListContextMenuManageMod": "Управление модами",
"ControllerSettingsStickRange": "Диапазон:",
- "DialogStopEmulationTitle": "Ryujinx - Остановить эмуляцию",
+ "DialogStopEmulationTitle": "Ryujinx - Остановка эмуляции",
"DialogStopEmulationMessage": "Вы уверены, что хотите остановить эмуляцию?",
- "SettingsTabCpu": "ЦП",
+ "SettingsTabCpu": "Процессор",
"SettingsTabAudio": "Аудио",
"SettingsTabNetwork": "Сеть",
"SettingsTabNetworkConnection": "Подключение к сети",
- "SettingsTabCpuCache": "Кэш ЦП",
- "SettingsTabCpuMemory": "Память ЦП",
+ "SettingsTabCpuCache": "Кэш процессора",
+ "SettingsTabCpuMemory": "Режим процессора",
"DialogUpdaterFlatpakNotSupportedMessage": "Пожалуйста, обновите Ryujinx через FlatHub.",
"UpdaterDisabledWarningTitle": "Средство обновления отключено",
"ControllerSettingsRotate90": "Повернуть на 90° по часовой стрелке",
"IconSize": "Размер обложек",
- "IconSizeTooltip": "Изменить размер обложек",
+ "IconSizeTooltip": "Меняет размер обложек",
"MenuBarOptionsShowConsole": "Показать консоль",
"ShaderCachePurgeError": "Ошибка очистки кэша шейдеров в {0}: {1}",
"UserErrorNoKeys": "Ключи не найдены",
@@ -578,7 +684,7 @@
"SettingsTabHotkeys": "Горячие клавиши",
"SettingsTabHotkeysHotkeys": "Горячие клавиши",
"SettingsTabHotkeysToggleVsyncHotkey": "Вертикальная синхронизация:",
- "SettingsTabHotkeysScreenshotHotkey": "Скриншот:",
+ "SettingsTabHotkeysScreenshotHotkey": "Сделать скриншот:",
"SettingsTabHotkeysShowUiHotkey": "Показать интерфейс:",
"SettingsTabHotkeysPauseHotkey": "Пауза эмуляции:",
"SettingsTabHotkeysToggleMuteHotkey": "Выключить звук:",
@@ -597,6 +703,7 @@
"UserProfileWindowTitle": "Менеджер учетных записей",
"CheatWindowTitle": "Менеджер читов",
"DlcWindowTitle": "Управление DLC для {0} ({1})",
+ "ModWindowTitle": "Управление модами для {0} ({1})",
"UpdateWindowTitle": "Менеджер обновлений игр",
"CheatWindowHeading": "Доступные читы для {0} [{1}]",
"BuildId": "ID версии:",
@@ -607,33 +714,33 @@
"Save": "Сохранить",
"Discard": "Отменить",
"Paused": "Приостановлено",
- "UserProfilesSetProfileImage": "Установить аватар профиля",
- "UserProfileEmptyNameError": "Имя обязательно",
+ "UserProfilesSetProfileImage": "Установить аватар",
+ "UserProfileEmptyNameError": "Необходимо ввести никнейм",
"UserProfileNoImageError": "Необходимо установить аватар",
"GameUpdateWindowHeading": "Доступные обновления для {0} ({1})",
"SettingsTabHotkeysResScaleUpHotkey": "Увеличить разрешение:",
"SettingsTabHotkeysResScaleDownHotkey": "Уменьшить разрешение:",
- "UserProfilesName": "Имя:",
+ "UserProfilesName": "Никнейм:",
"UserProfilesUserId": "ID пользователя:",
"SettingsTabGraphicsBackend": "Графический бэкенд",
- "SettingsTabGraphicsBackendTooltip": "Выберите бэкенд, который будет использован в эмуляторе.\n\nVulkan является лучшим выбором для всех современных графических карт с актуальными драйверами. В Vulkan также включена более быстрая компиляция шейдеров (меньше статтеров) для всех GPU.\n\nOpenGL может достичь лучших результатов на старых GPU Nvidia, на старых GPU AMD на Linux или на GPU с небольшим количеством VRAM, хотя статтеров при компиляции шейдеров будет больше.\n\nРекомендуется использовать Vulkan. Используйте OpenGL, если ваш GPU не поддерживает Vulkan даже с актуальными драйверами.",
- "SettingsEnableTextureRecompression": "Включить пережатие текстур",
- "SettingsEnableTextureRecompressionTooltip": "Сжатие ASTC текстур для уменьшения использования VRAM. \n\nИгры, использующие этот формат текстур: Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder и The Legend of Zelda: Tears of the Kingdom. \n\nНа GPU с 4GiB VRAM или менее возможны вылеты при запуске этих игр. \n\nВключите, только если у вас заканчивается VRAM в вышеупомянутых играх. Рекомендуется оставить выключенным.",
- "SettingsTabGraphicsPreferredGpu": "Предпочтительный GPU",
- "SettingsTabGraphicsPreferredGpuTooltip": "Выберите GPU, который будет использоваться с графическим бэкендом Vulkan.\n\nЭта настройка не влияет на GPU, который будет использовать OpenGL.\n\nЕсли вы не уверены что нужно выбрать, используйте графический процессор, помеченный как \"dGPU\". Если его нет, оставьте выбор по умолчанию.",
+ "SettingsTabGraphicsBackendTooltip": "Выберает бэкенд, который будет использован в эмуляторе.\n\nVulkan является лучшим выбором для всех современных графических карт с актуальными драйверами. В Vulkan также включена более быстрая компиляция шейдеров (меньше статтеров) для всех видеоадаптеров.\n\nПри использовании OpenGL можно достичь лучших результатов на старых видеоадаптерах Nvidia и AMD в Linux или на видеоадаптерах с небольшим количеством видеопамяти, хотя статтеров при компиляции шейдеров будет больше.\n\nРекомендуется использовать Vulkan. Используйте OpenGL, если ваш видеоадаптер не поддерживает Vulkan даже с актуальными драйверами.",
+ "SettingsEnableTextureRecompression": "Пережимать текстуры",
+ "SettingsEnableTextureRecompressionTooltip": "Сжатие ASTC текстур для уменьшения использования VRAM. \n\nИгры, использующие этот формат текстур: Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder и The Legend of Zelda: Tears of the Kingdom. \nНа видеоадаптерах с 4GiB видеопамяти или менее возможны вылеты при запуске этих игр. \n\nВключите, только если у вас заканчивается видеопамять в вышеупомянутых играх. \n\nРекомендуется оставить выключенным.",
+ "SettingsTabGraphicsPreferredGpu": "Предпочтительный видеоадаптер",
+ "SettingsTabGraphicsPreferredGpuTooltip": "Выберает видеоадаптер, который будет использоваться графическим бэкендом Vulkan.\n\nЭта настройка не влияет на видеоадаптер, который будет использоваться с OpenGL.\n\nЕсли вы не уверены что нужно выбрать, используйте графический процессор, помеченный как \"dGPU\". Если его нет, оставьте выбор по умолчанию.",
"SettingsAppRequiredRestartMessage": "Требуется перезапуск Ryujinx",
"SettingsGpuBackendRestartMessage": "Графический бэкенд или настройки графического процессора были изменены. Требуется перезапуск для вступления в силу изменений.",
"SettingsGpuBackendRestartSubMessage": "Перезапустить сейчас?",
- "RyujinxUpdaterMessage": "Вы хотите обновить Ryujinx до последней версии?",
+ "RyujinxUpdaterMessage": "Обновить Ryujinx до последней версии?",
"SettingsTabHotkeysVolumeUpHotkey": "Увеличить громкость:",
"SettingsTabHotkeysVolumeDownHotkey": "Уменьшить громкость:",
- "SettingsEnableMacroHLE": "Включить Macro HLE",
- "SettingsEnableMacroHLETooltip": "Высокоуровневая эмуляции макроса GPU.\n\nПовышает производительность, но может вызывать графические сбои в некоторых играх.\n\nРекомендуется оставить включенным.",
- "SettingsEnableColorSpacePassthrough": "Пропуск цветового пространства",
+ "SettingsEnableMacroHLE": "Использовать макрос высокоуровневой эмуляции видеоадаптера",
+ "SettingsEnableMacroHLETooltip": "Высокоуровневая эмуляции макрокода видеоадаптера.\n\nПовышает производительность, но может вызывать графические артефакты в некоторых играх.\n\nРекомендуется оставить включенным.",
+ "SettingsEnableColorSpacePassthrough": "Пропускать цветовое пространство",
"SettingsEnableColorSpacePassthroughTooltip": "Направляет бэкенд Vulkan на передачу информации о цвете без указания цветового пространства. Для пользователей с экранами с расширенной гаммой данная настройка приводит к получению более ярких цветов за счет снижения корректности цветопередачи.",
"VolumeShort": "Громкость",
"UserProfilesManageSaves": "Управление сохранениями",
- "DeleteUserSave": "Вы хотите удалить сохранения для этой игры?",
+ "DeleteUserSave": "Удалить сохранения для этой игры?",
"IrreversibleActionNote": "Данное действие является необратимым.",
"SaveManagerHeading": "Редактирование сохранений для {0} ({1})",
"SaveManagerTitle": "Менеджер сохранений",
@@ -644,10 +751,10 @@
"Recover": "Восстановление",
"UserProfilesRecoverHeading": "Были найдены сохранения для следующих аккаунтов",
"UserProfilesRecoverEmptyList": "Нет учетных записей для восстановления",
- "GraphicsAATooltip": "Применимое сглаживание для рендера.\n\nFXAA размывает большую часть изображения, SMAA попытается найти \"зазубренные\" края и сгладить их.\n\nНе рекомендуется использовать вместе с масштабирующим фильтром FSR.\n\nЭта опция может быть изменена во время игры по нажатию \"Применить\" ниже; Вы можете просто переместить окно настроек в сторону и поэкспериментировать, пока не найдёте подходящую настройку игры.\n\nРекомендуется использовать \"Нет\".",
+ "GraphicsAATooltip": "Применимое сглаживание для рендера.\n\nFXAA размывает большую часть изображения, SMAA попытается найти \"зазубренные\" края и сгладить их.\n\nНе рекомендуется использовать вместе с масштабирующим фильтром FSR.\n\nЭта опция может быть изменена во время игры по нажатию \"Применить\" ниже; \nВы можете просто переместить окно настроек в сторону и поэкспериментировать, пока не найдёте подходящую настройку игры.\n\nРекомендуется использовать \"Нет\".",
"GraphicsAALabel": "Сглаживание:",
"GraphicsScalingFilterLabel": "Интерполяция:",
- "GraphicsScalingFilterTooltip": "Выберите фильтрацию, которая будет применяться при масштабировании.\n\n\"Билинейная\" хорошо работает для 3D-игр и является настройкой по умолчанию.\n\n\"Ступенчатая\" рекомендуется для пиксельных игр.\n\n\"FSR\" это фильтр резкости, который не рекомендуется использовать с FXAA или SMAA.\n\nЭта опция может быть изменена во время игры по нажатию кнопки \"Применить\" ниже; Вы можете просто переместить окно настроек в сторону и поэкспериментировать, пока не подберете подходящие настройки для конкретной игры.\n\nРекомендуется использовать \"Билинейная\".",
+ "GraphicsScalingFilterTooltip": "Фильтрация текстур, которая будет применяться при масштабировании.\n\nБилинейная хорошо работает для 3D-игр и является настройкой по умолчанию.\n\nСтупенчатая рекомендуется для пиксельных игр.\n\nFSR это фильтр резкости, который не рекомендуется использовать с FXAA или SMAA.\n\nЭта опция может быть изменена во время игры по нажатию кнопки \"Применить\" ниже; \nВы можете просто переместить окно настроек в сторону и поэкспериментировать, пока не подберете подходящие настройки для конкретной игры.\n\nРекомендуется использовать \"Билинейная\".",
"GraphicsScalingFilterBilinear": "Билинейная",
"GraphicsScalingFilterNearest": "Ступенчатая",
"GraphicsScalingFilterFsr": "FSR",
@@ -667,7 +774,7 @@
"AboutChangelogButtonTooltipMessage": "Нажмите, чтобы открыть список изменений для этой версии",
"SettingsTabNetworkMultiplayer": "Мультиплеер",
"MultiplayerMode": "Режим:",
- "MultiplayerModeTooltip": "Изменение многопользовательского режима LDN.\n\nLdnMitm модифицирует функциональность локальной беспроводной/игры на одном устройстве в играх, позволяя играть с другими пользователями Ryujinx или взломанными консолями Nintendo Switch с установленным модулем ldn_mitm, находящимися в одной локальной сети друг с другом.\n\nМногопользовательская игра требует наличия у всех игроков одной и той же версии игры (т.е. Super Smash Bros. Ultimate v13.0.1 не может подключиться к v13.0.0).\n\nРекомендуется оставить отключенным.",
+ "MultiplayerModeTooltip": "Меняет многопользовательский режим LDN.\n\nLdnMitm модифицирует функциональность локальной беспроводной/игры на одном устройстве в играх, позволяя играть с другими пользователями Ryujinx или взломанными консолями Nintendo Switch с установленным модулем ldn_mitm, находящимися в одной локальной сети друг с другом.\n\nМногопользовательская игра требует наличия у всех игроков одной и той же версии игры (т.е. Super Smash Bros. Ultimate v13.0.1 не может подключиться к v13.0.0).\n\nРекомендуется оставить отключенным.",
"MultiplayerModeDisabled": "Отключено",
"MultiplayerModeLdnMitm": "ldn_mitm"
}
diff --git a/src/Ryujinx/Assets/Locales/th_TH.json b/src/Ryujinx/Assets/Locales/th_TH.json
index 23745ad94..629442269 100644
--- a/src/Ryujinx/Assets/Locales/th_TH.json
+++ b/src/Ryujinx/Assets/Locales/th_TH.json
@@ -11,27 +11,31 @@
"MenuBarFile": "ไฟล์",
"MenuBarFileOpenFromFile": "โหลดแอปพลิเคชั่นจากไฟล์",
"MenuBarFileOpenUnpacked": "โหลดเกมที่คลายแพ็กแล้ว",
- "MenuBarFileOpenEmuFolder": "เปิดโฟลเดอร์ รียูจินซ์",
+ "MenuBarFileOpenEmuFolder": "เปิดโฟลเดอร์ Ryujinx",
"MenuBarFileOpenLogsFolder": "เปิดโฟลเดอร์ Logs",
- "MenuBarFileExit": "ออก",
+ "MenuBarFileExit": "_ออก",
"MenuBarOptions": "_ตัวเลือก",
"MenuBarOptionsToggleFullscreen": "สลับการแสดงผลแบบเต็มหน้าจอ",
"MenuBarOptionsStartGamesInFullscreen": "เริ่มเกมในโหมดเต็มหน้าจอ",
"MenuBarOptionsStopEmulation": "หยุดการจำลอง",
- "MenuBarOptionsSettings": "การตั้งค่า",
- "MenuBarOptionsManageUserProfiles": "จัดการโปรไฟล์ผู้ใช้งาน",
+ "MenuBarOptionsSettings": "_ตั้งค่า",
+ "MenuBarOptionsManageUserProfiles": "_จัดการโปรไฟล์ผู้ใช้งาน",
"MenuBarActions": "การดำเนินการ",
"MenuBarOptionsSimulateWakeUpMessage": "จำลองข้อความปลุก",
- "MenuBarActionsScanAmiibo": "สแกนหา อะมิโบ",
+ "MenuBarActionsScanAmiibo": "สแกนหา Amiibo",
"MenuBarTools": "_เครื่องมือ",
"MenuBarToolsInstallFirmware": "ติดตั้งเฟิร์มแวร์",
"MenuBarFileToolsInstallFirmwareFromFile": "ติดตั้งเฟิร์มแวร์จาก ไฟล์ XCI หรือ ไฟล์ ZIP",
"MenuBarFileToolsInstallFirmwareFromDirectory": "ติดตั้งเฟิร์มแวร์จากไดเร็กทอรี",
"MenuBarToolsManageFileTypes": "จัดการประเภทไฟล์",
- "MenuBarToolsInstallFileTypes": "ติดตั้งประเภทไฟล์",
- "MenuBarToolsUninstallFileTypes": "ถอนการติดตั้งประเภทไฟล์",
+ "MenuBarToolsInstallFileTypes": "ติดตั้งตามประเภทของไฟล์",
+ "MenuBarToolsUninstallFileTypes": "ถอนการติดตั้งตามประเภทของไฟล์",
+ "MenuBarView": "_View",
+ "MenuBarViewWindow": "Window Size",
+ "MenuBarViewWindow720": "720p",
+ "MenuBarViewWindow1080": "1080p",
"MenuBarHelp": "_ช่วยเหลือ",
- "MenuBarHelpCheckForUpdates": "ตรวจหาการอัพเดต",
+ "MenuBarHelpCheckForUpdates": "ตรวจสอบอัปเดต",
"MenuBarHelpAbout": "เกี่ยวกับ",
"MenuSearch": "กำลังค้นหา...",
"GameListHeaderFavorite": "ชื่นชอบ",
@@ -39,31 +43,31 @@
"GameListHeaderApplication": "ชื่อ",
"GameListHeaderDeveloper": "ผู้พัฒนา",
"GameListHeaderVersion": "เวอร์ชั่น",
- "GameListHeaderTimePlayed": "เวลาที่เล่นไปแล้ว",
+ "GameListHeaderTimePlayed": "เล่นไปแล้ว",
"GameListHeaderLastPlayed": "เล่นล่าสุด",
"GameListHeaderFileExtension": "นามสกุลไฟล์",
"GameListHeaderFileSize": "ขนาดไฟล์",
- "GameListHeaderPath": "ที่เก็บไฟล์",
+ "GameListHeaderPath": "ที่อยู่ไฟล์",
"GameListContextMenuOpenUserSaveDirectory": "เปิดไดเร็กทอรี่บันทึกของผู้ใช้",
"GameListContextMenuOpenUserSaveDirectoryToolTip": "เปิดไดเร็กทอรี่ซึ่งมีการบันทึกผู้ใช้ของแอปพลิเคชัน",
- "GameListContextMenuOpenDeviceSaveDirectory": "เปิดไดเร็กทอรีบันทึกของอุปกรณ์",
+ "GameListContextMenuOpenDeviceSaveDirectory": "เปิดไดเร็กทอรี่บันทึกของอุปกรณ์",
"GameListContextMenuOpenDeviceSaveDirectoryToolTip": "เปิดไดเรกทอรี่ซึ่งมีบันทึกอุปกรณ์ของแอปพลิเคชัน",
- "GameListContextMenuOpenBcatSaveDirectory": "เปิดไดเรกทอรีบันทึก BCAT",
+ "GameListContextMenuOpenBcatSaveDirectory": "เปิดไดเรกทอรี่บันทึก BCAT",
"GameListContextMenuOpenBcatSaveDirectoryToolTip": "เปิดไดเรกทอรี่ซึ่งมีการบันทึก BCAT ของแอปพลิเคชัน",
- "GameListContextMenuManageTitleUpdates": "จัดการ การอัปเดตหัวข้อ",
+ "GameListContextMenuManageTitleUpdates": "จัดการอัปเดตตามหัวข้อ",
"GameListContextMenuManageTitleUpdatesToolTip": "เปิดหน้าต่างการจัดการการอัพเดตหัวข้อ",
"GameListContextMenuManageDlc": "จัดการ DLC",
- "GameListContextMenuManageDlcToolTip": "เปิดหน้าต่างการจัดการ DLC",
- "GameListContextMenuCacheManagement": "การบริหารจัดการแคช",
+ "GameListContextMenuManageDlcToolTip": "เปิดหน้าต่างจัดการ DLC",
+ "GameListContextMenuCacheManagement": "จัดการ แคช",
"GameListContextMenuCacheManagementPurgePptc": "เพิ่มเข้าคิวงาน PPTC ที่สร้างใหม่",
"GameListContextMenuCacheManagementPurgePptcToolTip": "ทริกเกอร์ PPTC ให้สร้างใหม่ในเวลาบูตเมื่อเปิดตัวเกมครั้งถัดไป",
- "GameListContextMenuCacheManagementPurgeShaderCache": "ล้าง เชเดอร์แคช",
- "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "ลบ เชเดอร์แคช ของแอปพลิเคชัน",
- "GameListContextMenuCacheManagementOpenPptcDirectory": "เปิดไดเรกทอรี่่ PPTC",
- "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "เปิดไดเร็กทอรีที่มี PPTC แคช ของแอปพลิเคชัน",
- "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "เปิดไดเรกทอรี่ เชเดอร์แคช",
- "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "เปิดไดเรกทอรี่ที่มี แคชเชเดอร์ ของแอปพลิเคชัน",
- "GameListContextMenuExtractData": "แยกข้อมูล",
+ "GameListContextMenuCacheManagementPurgeShaderCache": "ล้างแคช พื้นผิวและแสงเงา",
+ "GameListContextMenuCacheManagementPurgeShaderCacheToolTip": "ลบแคช พื้นผิวและแสงเงา ของแอปพลิเคชัน",
+ "GameListContextMenuCacheManagementOpenPptcDirectory": "เปิดไดเรกทอรี่ PPTC",
+ "GameListContextMenuCacheManagementOpenPptcDirectoryToolTip": "เปิดไดเร็กทอรี่ PPTC แคช ของแอปพลิเคชัน",
+ "GameListContextMenuCacheManagementOpenShaderCacheDirectory": "เปิดไดเรกทอรี่ แคช พื้นผิวและแสงเงา",
+ "GameListContextMenuCacheManagementOpenShaderCacheDirectoryToolTip": "เปิดไดเรกทอรี่ แคช พื้นผิวและแสงเงา ของแอปพลิเคชัน",
+ "GameListContextMenuExtractData": "แยกส่วนข้อมูล",
"GameListContextMenuExtractDataExeFS": "ExeFS",
"GameListContextMenuExtractDataExeFSToolTip": "แยกส่วน ExeFS ออกจากการกำหนดค่าปัจจุบันของแอปพลิเคชัน (รวมถึงการอัปเดต)",
"GameListContextMenuExtractDataRomFS": "RomFS",
@@ -73,28 +77,29 @@
"GameListContextMenuCreateShortcut": "สร้างทางลัดของแอปพลิเคชัน",
"GameListContextMenuCreateShortcutToolTip": "สร้างทางลัดบนเดสก์ท็อปที่เรียกใช้แอปพลิเคชันที่เลือก",
"GameListContextMenuCreateShortcutToolTipMacOS": "สร้างทางลัดในโฟลเดอร์ Applications ของ macOS ที่เรียกใช้ Application ที่เลือก",
- "GameListContextMenuOpenModsDirectory": "เปิดไดเรกทอรี่ Mods",
- "GameListContextMenuOpenModsDirectoryToolTip": "เปิดไดเร็กทอรีซึ่งมี Mods ของแอปพลิเคชัน",
- "GameListContextMenuOpenSdModsDirectory": "เปิดไดเร็กทอรี Mods Atmosphere",
- "GameListContextMenuOpenSdModsDirectoryToolTip": "เปิดไดเร็กทอรี Atmosphere ของการ์ด SD สำรองซึ่งมี Mods ของแอปพลิเคชัน มีประโยชน์สำหรับ mods ที่บรรจุมากับฮาร์ดแวร์จริง",
+ "GameListContextMenuOpenModsDirectory": "เปิดไดเร็กทอรี่ Mods",
+ "GameListContextMenuOpenModsDirectoryToolTip": "เปิดไดเร็กทอรี่ Mods ของแอปพลิเคชัน",
+ "GameListContextMenuOpenSdModsDirectory": "เปิดไดเร็กทอรี่ Mods Atmosphere",
+ "GameListContextMenuOpenSdModsDirectoryToolTip": "เปิดไดเร็กทอรี่ Atmosphere ของการ์ด SD สำรองซึ่งมี Mods ของแอปพลิเคชัน มีประโยชน์สำหรับ Mods ที่บรรจุมากับฮาร์ดแวร์จริง",
"StatusBarGamesLoaded": "เกมส์โหลดแล้ว {0}/{1}",
"StatusBarSystemVersion": "เวอร์ชั่นของระบบ: {0}",
- "LinuxVmMaxMapCountDialogTitle": "ตรวจพบขีดจำกัดต่ำสุดสำหรับการแมปหน่วยความจำ",
+ "LinuxVmMaxMapCountDialogTitle": "ตรวจพบขีดจำกัดต่ำสุด สำหรับการแมปหน่วยความจำ",
"LinuxVmMaxMapCountDialogTextPrimary": "คุณต้องการที่จะเพิ่มค่า vm.max_map_count ไปยัง {0}",
"LinuxVmMaxMapCountDialogTextSecondary": "บางเกมอาจพยายามสร้างการแมปหน่วยความจำมากกว่าที่ได้รับอนุญาตในปัจจุบัน รียูจินซ์ จะปิดตัวลงเมื่อเกินขีดจำกัดนี้",
"LinuxVmMaxMapCountDialogButtonUntilRestart": "ใช่, จนกว่าจะรีสตาร์ทครั้งถัดไป",
"LinuxVmMaxMapCountDialogButtonPersistent": "ใช่, อย่างถาวร",
"LinuxVmMaxMapCountWarningTextPrimary": "จำนวนสูงสุดของการแม็ปหน่วยความจำ ต่ำกว่าที่แนะนำ",
"LinuxVmMaxMapCountWarningTextSecondary": "ค่าปัจจุบันของ vm.max_map_count ({0}) มีค่าต่ำกว่า {1} บางเกมอาจพยายามสร้างการแมปหน่วยความจำมากกว่าที่ได้รับอนุญาตในปัจจุบัน รียูจินซ์ จะปิดตัวลงเมื่อเกินขีดจำกัดนี้\n\nคุณอาจต้องการเพิ่มขีดจำกัดด้วยตนเองหรือติดตั้ง pkexec ซึ่งอนุญาตให้ ริวจินซ์ เพื่อช่วยเหลือคุณได้",
- "Settings": "การตั้งค่า",
+ "Settings": "ตั้งค่า",
"SettingsTabGeneral": "หน้าจอผู้ใช้",
"SettingsTabGeneralGeneral": "ทั่วไป",
"SettingsTabGeneralEnableDiscordRichPresence": "เปิดใช้งาน Discord Rich Presence",
"SettingsTabGeneralCheckUpdatesOnLaunch": "ตรวจหาการอัปเดตเมื่อเปิดโปรแกรม",
"SettingsTabGeneralShowConfirmExitDialog": "แสดง \"ยืนยันการออก\" กล่องข้อความโต้ตอบ",
+ "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position",
"SettingsTabGeneralHideCursor": "ซ่อน เคอร์เซอร์:",
"SettingsTabGeneralHideCursorNever": "ไม่มี",
- "SettingsTabGeneralHideCursorOnIdle": "ซ่อนอัตโนมัติ",
+ "SettingsTabGeneralHideCursorOnIdle": "เมื่อไม่ได้ใช้",
"SettingsTabGeneralHideCursorAlways": "ตลอดเวลา",
"SettingsTabGeneralGameDirectories": "ไดเรกทอรี่ของเกม",
"SettingsTabGeneralAdd": "เพิ่ม",
@@ -127,13 +132,13 @@
"SettingsTabSystemSystemLanguageLatinAmericanSpanish": "สเปน (ลาตินอเมริกา)",
"SettingsTabSystemSystemLanguageSimplifiedChinese": "จีน (ตัวย่อ)",
"SettingsTabSystemSystemLanguageTraditionalChinese": "จีน (ดั้งเดิม)",
- "SettingsTabSystemSystemTimeZone": "โซนเวลาของระบบ:",
+ "SettingsTabSystemSystemTimeZone": "เขตเวลาของระบบ:",
"SettingsTabSystemSystemTime": "เวลาของระบบ:",
"SettingsTabSystemEnableVsync": "VSync",
- "SettingsTabSystemEnablePptc": "PPTC (แคชการแปลแบบถาวรที่มีโปรไฟล์)",
- "SettingsTabSystemEnableFsIntegrityChecks": "การตรวจสอบความถูกต้องของ FS",
- "SettingsTabSystemAudioBackend": "แบ็กเอนด์เสียง:",
- "SettingsTabSystemAudioBackendDummy": "ดัมมี่",
+ "SettingsTabSystemEnablePptc": "PPTC (แคชโปรไฟล์การแปลแบบถาวร)",
+ "SettingsTabSystemEnableFsIntegrityChecks": "ตรวจสอบความถูกต้องของ FS",
+ "SettingsTabSystemAudioBackend": "ระบบเสียงเบื้องหลัง:",
+ "SettingsTabSystemAudioBackendDummy": "Dummy",
"SettingsTabSystemAudioBackendOpenAL": "OpenAL",
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
"SettingsTabSystemAudioBackendSDL2": "SDL2",
@@ -142,9 +147,9 @@
"SettingsTabSystemExpandDramSize": "ใช้รูปแบบหน่วยความจำสำรอง (โหมดนักพัฒนา)",
"SettingsTabSystemIgnoreMissingServices": "ไม่สนใจบริการที่ขาดหายไป",
"SettingsTabGraphics": "กราฟิก",
- "SettingsTabGraphicsAPI": "เอพีไอของกราฟิก",
- "SettingsTabGraphicsEnableShaderCache": "เปิดใช้งาน เชเดอร์ แคช",
- "SettingsTabGraphicsAnisotropicFiltering": "ตัวกรองแบบ แอนไอโซทรอปิก:",
+ "SettingsTabGraphicsAPI": "กราฟฟิก API",
+ "SettingsTabGraphicsEnableShaderCache": "เปิดใช้งาน แคชพื้นผิวและแสงเงา",
+ "SettingsTabGraphicsAnisotropicFiltering": "ตัวกรองแบบ Anisotropic:",
"SettingsTabGraphicsAnisotropicFilteringAuto": "อัตโนมัติ",
"SettingsTabGraphicsAnisotropicFiltering2x": "2x",
"SettingsTabGraphicsAnisotropicFiltering4x": "4x",
@@ -164,26 +169,26 @@
"SettingsTabGraphicsAspectRatio32x9": "32:9",
"SettingsTabGraphicsAspectRatioStretch": "ยืดภาพเพื่อให้พอดีกับหน้าต่าง",
"SettingsTabGraphicsDeveloperOptions": "ตัวเลือกนักพัฒนา",
- "SettingsTabGraphicsShaderDumpPath": "ที่เก็บไฟล์ดัมพ์ของ เชเดอร์กราฟิก:",
- "SettingsTabLogging": "การบันทึก",
- "SettingsTabLoggingLogging": "การบันทึก",
- "SettingsTabLoggingEnableLoggingToFile": "เปิดใช้งาน การบันทึกไปยังไฟล์",
- "SettingsTabLoggingEnableStubLogs": "เปิดใช้งาน บันทึกของต้นขั้ว",
- "SettingsTabLoggingEnableInfoLogs": "เปิดใช้งาน บันทึกของข้อมูล",
- "SettingsTabLoggingEnableWarningLogs": "เปิดใช้งาน บันทึกคำเตือน",
- "SettingsTabLoggingEnableErrorLogs": "เปิดใช้งาน บันทึกข้อผิดพลาด",
- "SettingsTabLoggingEnableTraceLogs": "เปิดใช้งาน บันทึกการติดตาม",
+ "SettingsTabGraphicsShaderDumpPath": "ที่เก็บ ดัมพ์ไฟล์ พื้นผิวและแสงเงา:",
+ "SettingsTabLogging": "ประวัติ",
+ "SettingsTabLoggingLogging": "ประวัติ",
+ "SettingsTabLoggingEnableLoggingToFile": "เปิดใช้งาน ประวัติ ไปยังไฟล์",
+ "SettingsTabLoggingEnableStubLogs": "เปิดใช้งาน ประวัติ",
+ "SettingsTabLoggingEnableInfoLogs": "เปิดใช้งาน ประวัติการใช้งาน",
+ "SettingsTabLoggingEnableWarningLogs": "เปิดใช้งาน ประวัติคำเตือน",
+ "SettingsTabLoggingEnableErrorLogs": "เปิดใช้งาน ประวัติข้อผิดพลาด",
+ "SettingsTabLoggingEnableTraceLogs": "เปิดใช้งาน ประวัติการติดตาม",
"SettingsTabLoggingEnableGuestLogs": "เปิดใช้งาน บันทึกของผู้เยี่ยมชม",
- "SettingsTabLoggingEnableFsAccessLogs": "เปิดใช้งาน บันทึกการเข้าถึง Fs",
- "SettingsTabLoggingFsGlobalAccessLogMode": "โหมดบันทึกการเข้าถึงส่วนกลาง:",
+ "SettingsTabLoggingEnableFsAccessLogs": "เปิดใช้งาน ประวัติการเข้าถึง Fs",
+ "SettingsTabLoggingFsGlobalAccessLogMode": "โหมด ประวัติการเข้าถึงส่วนกลาง:",
"SettingsTabLoggingDeveloperOptions": "ตัวเลือกนักพัฒนา",
"SettingsTabLoggingDeveloperOptionsNote": "คำเตือน: จะทำให้ประสิทธิภาพลดลง",
- "SettingsTabLoggingGraphicsBackendLogLevel": "ระดับการบันทึก แบ็กเอนด์กราฟิก:",
+ "SettingsTabLoggingGraphicsBackendLogLevel": "ระดับการบันทึกประวัติ กราฟิกเบื้องหลัง:",
"SettingsTabLoggingGraphicsBackendLogLevelNone": "ไม่มี",
"SettingsTabLoggingGraphicsBackendLogLevelError": "ผิดพลาด",
- "SettingsTabLoggingGraphicsBackendLogLevelPerformance": "ชะลอตัว",
+ "SettingsTabLoggingGraphicsBackendLogLevelPerformance": "ช้าลง",
"SettingsTabLoggingGraphicsBackendLogLevelAll": "ทั้งหมด",
- "SettingsTabLoggingEnableDebugLogs": "เปิดใช้งานบันทึกการแก้ไขข้อบกพร่อง",
+ "SettingsTabLoggingEnableDebugLogs": "เปิดใช้งาน ประวัติแก้ไขข้อบกพร่อง",
"SettingsTabInput": "ป้อนข้อมูล",
"SettingsTabInputEnableDockedMode": "ด็อกโหมด",
"SettingsTabInputDirectKeyboardAccess": "เข้าถึงคีย์บอร์ดโดยตรง",
@@ -261,11 +266,112 @@
"ControllerSettingsMotionControllerSlot": "ช่องเสียบ คอนโทรลเลอร์:",
"ControllerSettingsMotionMirrorInput": "นำเข้าการสะท้อน การควบคุม",
"ControllerSettingsMotionRightJoyConSlot": "ช่องเสียบ จอยคอน ด้านขวา:",
- "ControllerSettingsMotionServerHost": "เซิร์ฟเวอร์โฮสต์:",
+ "ControllerSettingsMotionServerHost": "เจ้าของเซิร์ฟเวอร์:",
"ControllerSettingsMotionGyroSensitivity": "ความไวของไจโร:",
- "ControllerSettingsMotionGyroDeadzone": "โซนที่ไม่ทำงานของไจโร:",
+ "ControllerSettingsMotionGyroDeadzone": "ส่วนไม่ทำงานของไจโร:",
"ControllerSettingsSave": "บันทึก",
"ControllerSettingsClose": "ปิด",
+ "KeyUnknown": "Unknown",
+ "KeyShiftLeft": "Shift Left",
+ "KeyShiftRight": "Shift Right",
+ "KeyControlLeft": "Ctrl Left",
+ "KeyMacControlLeft": "⌃ Left",
+ "KeyControlRight": "Ctrl Right",
+ "KeyMacControlRight": "⌃ Right",
+ "KeyAltLeft": "Alt Left",
+ "KeyMacAltLeft": "⌥ Left",
+ "KeyAltRight": "Alt Right",
+ "KeyMacAltRight": "⌥ Right",
+ "KeyWinLeft": "⊞ Left",
+ "KeyMacWinLeft": "⌘ Left",
+ "KeyWinRight": "⊞ Right",
+ "KeyMacWinRight": "⌘ Right",
+ "KeyMenu": "Menu",
+ "KeyUp": "Up",
+ "KeyDown": "Down",
+ "KeyLeft": "Left",
+ "KeyRight": "Right",
+ "KeyEnter": "Enter",
+ "KeyEscape": "Escape",
+ "KeySpace": "Space",
+ "KeyTab": "Tab",
+ "KeyBackSpace": "Backspace",
+ "KeyInsert": "Insert",
+ "KeyDelete": "Delete",
+ "KeyPageUp": "Page Up",
+ "KeyPageDown": "Page Down",
+ "KeyHome": "Home",
+ "KeyEnd": "End",
+ "KeyCapsLock": "Caps Lock",
+ "KeyScrollLock": "Scroll Lock",
+ "KeyPrintScreen": "Print Screen",
+ "KeyPause": "Pause",
+ "KeyNumLock": "Num Lock",
+ "KeyClear": "Clear",
+ "KeyKeypad0": "Keypad 0",
+ "KeyKeypad1": "Keypad 1",
+ "KeyKeypad2": "Keypad 2",
+ "KeyKeypad3": "Keypad 3",
+ "KeyKeypad4": "Keypad 4",
+ "KeyKeypad5": "Keypad 5",
+ "KeyKeypad6": "Keypad 6",
+ "KeyKeypad7": "Keypad 7",
+ "KeyKeypad8": "Keypad 8",
+ "KeyKeypad9": "Keypad 9",
+ "KeyKeypadDivide": "Keypad Divide",
+ "KeyKeypadMultiply": "Keypad Multiply",
+ "KeyKeypadSubtract": "Keypad Subtract",
+ "KeyKeypadAdd": "Keypad Add",
+ "KeyKeypadDecimal": "Keypad Decimal",
+ "KeyKeypadEnter": "Keypad Enter",
+ "KeyNumber0": "0",
+ "KeyNumber1": "1",
+ "KeyNumber2": "2",
+ "KeyNumber3": "3",
+ "KeyNumber4": "4",
+ "KeyNumber5": "5",
+ "KeyNumber6": "6",
+ "KeyNumber7": "7",
+ "KeyNumber8": "8",
+ "KeyNumber9": "9",
+ "KeyTilde": "~",
+ "KeyGrave": "`",
+ "KeyMinus": "-",
+ "KeyPlus": "+",
+ "KeyBracketLeft": "[",
+ "KeyBracketRight": "]",
+ "KeySemicolon": ";",
+ "KeyQuote": "\"",
+ "KeyComma": ",",
+ "KeyPeriod": ".",
+ "KeySlash": "/",
+ "KeyBackSlash": "\\",
+ "KeyUnbound": "Unbound",
+ "GamepadLeftStick": "L Stick Button",
+ "GamepadRightStick": "R Stick Button",
+ "GamepadLeftShoulder": "Left Shoulder",
+ "GamepadRightShoulder": "Right Shoulder",
+ "GamepadLeftTrigger": "Left Trigger",
+ "GamepadRightTrigger": "Right Trigger",
+ "GamepadDpadUp": "Up",
+ "GamepadDpadDown": "Down",
+ "GamepadDpadLeft": "Left",
+ "GamepadDpadRight": "Right",
+ "GamepadMinus": "-",
+ "GamepadPlus": "+",
+ "GamepadGuide": "Guide",
+ "GamepadMisc1": "Misc",
+ "GamepadPaddle1": "Paddle 1",
+ "GamepadPaddle2": "Paddle 2",
+ "GamepadPaddle3": "Paddle 3",
+ "GamepadPaddle4": "Paddle 4",
+ "GamepadTouchpad": "Touchpad",
+ "GamepadSingleLeftTrigger0": "Left Trigger 0",
+ "GamepadSingleRightTrigger0": "Right Trigger 0",
+ "GamepadSingleLeftTrigger1": "Left Trigger 1",
+ "GamepadSingleRightTrigger1": "Right Trigger 1",
+ "StickLeft": "Left Stick",
+ "StickRight": "Right Stick",
"UserProfilesSelectedUserProfile": "โปรไฟล์ผู้ใช้งานที่เลือก:",
"UserProfilesSaveProfileName": "บันทึกชื่อโปรไฟล์",
"UserProfilesChangeProfileImage": "เปลี่ยนรูปโปรไฟล์",
@@ -273,25 +379,25 @@
"UserProfilesAddNewProfile": "สร้างโปรไฟล์ใหม่",
"UserProfilesDelete": "ลบ",
"UserProfilesClose": "ปิด",
- "ProfileNameSelectionWatermark": "เลือกชื่อเล่น",
- "ProfileImageSelectionTitle": "เลือกรูปโปรไฟล์ของคุณ",
- "ProfileImageSelectionHeader": "เลือกรูปโปรไฟล์",
- "ProfileImageSelectionNote": "คุณสามารถนำเข้ารูปโปรไฟล์ที่กำหนดเอง หรือเลือกอวาต้าจากเฟิร์มแวร์ระบบได้",
- "ProfileImageSelectionImportImage": "นำเข้าไฟล์รูปภาพ",
- "ProfileImageSelectionSelectAvatar": "เลือกรูปอวาต้าเฟิร์มแวร์",
+ "ProfileNameSelectionWatermark": "เลือก ชื่อเล่น",
+ "ProfileImageSelectionTitle": "เลือก รูปโปรไฟล์ ของคุณ",
+ "ProfileImageSelectionHeader": "เลือก รูปโปรไฟล์",
+ "ProfileImageSelectionNote": "คุณสามารถนำเข้ารูปโปรไฟล์ที่กำหนดเอง หรือ เลือกอวาต้าจากเฟิร์มแวร์ระบบได้",
+ "ProfileImageSelectionImportImage": "นำเข้า ไฟล์รูปภาพ",
+ "ProfileImageSelectionSelectAvatar": "เลือก รูปอวาต้า เฟิร์มแวร์",
"InputDialogTitle": "กล่องโต้ตอบการป้อนข้อมูล",
"InputDialogOk": "ตกลง",
"InputDialogCancel": "ยกเลิก",
- "InputDialogAddNewProfileTitle": "เลือกชื่อโปรไฟล์",
+ "InputDialogAddNewProfileTitle": "เลือก ชื่อโปรไฟล์",
"InputDialogAddNewProfileHeader": "กรุณาใส่ชื่อโปรไฟล์",
"InputDialogAddNewProfileSubtext": "(ความยาวสูงสุด: {0})",
- "AvatarChoose": "เลือกรูปอวาต้าของคุณ",
+ "AvatarChoose": "เลือก รูปอวาต้า ของคุณ",
"AvatarSetBackgroundColor": "ตั้งค่าสีพื้นหลัง",
"AvatarClose": "ปิด",
- "ControllerSettingsLoadProfileToolTip": "โหลดโปรไฟล์",
- "ControllerSettingsAddProfileToolTip": "เพิ่มโปรไฟล์",
- "ControllerSettingsRemoveProfileToolTip": "ลบโปรไฟล์",
- "ControllerSettingsSaveProfileToolTip": "บันทึกโปรไฟล์",
+ "ControllerSettingsLoadProfileToolTip": "โหลด โปรไฟล์",
+ "ControllerSettingsAddProfileToolTip": "เพิ่ม โปรไฟล์",
+ "ControllerSettingsRemoveProfileToolTip": "ลบ โปรไฟล์",
+ "ControllerSettingsSaveProfileToolTip": "บันทึก โปรไฟล์",
"MenuBarFileToolsTakeScreenshot": "ถ่ายภาพหน้าจอ",
"MenuBarFileToolsHideUi": "ซ่อน UI",
"GameListContextMenuRunApplication": "เรียกใช้แอปพลิเคชัน",
@@ -343,11 +449,11 @@
"DialogFirmwareInstallEmbeddedMessage": "คุณต้องการติดตั้งเฟิร์มแวร์ที่ฝังอยู่ในเกมนี้หรือไม่? (เฟิร์มแวร์ {0})",
"DialogFirmwareInstallEmbeddedSuccessMessage": "ไม่พบเฟิร์มแวร์ที่ติดตั้งไว้ แต่ รียูจินซ์ สามารถติดตั้งเฟิร์มแวร์ได้ {0} จากเกมที่ให้มา\nตอนนี้โปรแกรมจำลองจะเริ่มทำงาน",
"DialogFirmwareNoFirmwareInstalledMessage": "ไม่มีการติดตั้งเฟิร์มแวร์",
- "DialogFirmwareInstalledMessage": "เฟิร์มแวร์ทำการติดตั้งแล้ว {0}",
- "DialogInstallFileTypesSuccessMessage": "ติดตั้งประเภทไฟล์สำเร็จแล้ว!",
- "DialogInstallFileTypesErrorMessage": "ติดตั้งประเภทไฟล์ไม่สำเร็จ",
- "DialogUninstallFileTypesSuccessMessage": "ถอนการติดตั้งประเภทไฟล์สำเร็จแล้ว!",
- "DialogUninstallFileTypesErrorMessage": "ไม่สามารถถอนการติดตั้งประเภทไฟล์ได้",
+ "DialogFirmwareInstalledMessage": "เฟิร์มแวร์ติดตั้งแล้ว {0}",
+ "DialogInstallFileTypesSuccessMessage": "ติดตั้งตามประเภทของไฟล์สำเร็จแล้ว!",
+ "DialogInstallFileTypesErrorMessage": "ติดตั้งตามประเภทของไฟล์ไม่สำเร็จ",
+ "DialogUninstallFileTypesSuccessMessage": "ถอนการติดตั้งตามประเภทของไฟล์สำเร็จแล้ว!",
+ "DialogUninstallFileTypesErrorMessage": "ไม่สามารถถอนการติดตั้งตามประเภทของไฟล์ได้",
"DialogOpenSettingsWindowLabel": "เปิดหน้าต่างการตั้งค่า",
"DialogControllerAppletTitle": "แอพเพล็ตคอนโทรลเลอร์",
"DialogMessageDialogErrorExceptionMessage": "เกิดข้อผิดพลาดในการแสดงกล่องโต้ตอบข้อความ: {0}",
@@ -369,7 +475,7 @@
"DialogShaderDeletionMessage": "คุณกำลังจะลบ เชเดอร์แคช:\n\n{0}\n\nคุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อหรือไม่?",
"DialogShaderDeletionErrorMessage": "เกิดข้อผิดพลาดในการล้าง เชเดอร์แคช {0}: {1}",
"DialogRyujinxErrorMessage": "รียูจินซ์ พบข้อผิดพลาด",
- "DialogInvalidTitleIdErrorMessage": "ข้อผิดพลาดของ ยูไอ: เกมที่เลือกไม่มีชื่อ ID ที่ถูกต้อง",
+ "DialogInvalidTitleIdErrorMessage": "ข้อผิดพลาดของ UI: เกมที่เลือกไม่มีชื่อ ID ที่ถูกต้อง",
"DialogFirmwareInstallerFirmwareNotFoundErrorMessage": "ไม่พบเฟิร์มแวร์ของระบบที่ถูกต้อง {0}.",
"DialogFirmwareInstallerFirmwareInstallTitle": "ติดตั้งเฟิร์มแวร์ {0}",
"DialogFirmwareInstallerFirmwareInstallMessage": "นี่คื่อเวอร์ชั่นของระบบ {0} ที่ได้รับการติดตั้งเมื่อเร็วๆ นี้",
@@ -401,7 +507,7 @@
"DialogModManagerDeletionWarningMessage": "คุณกำลังจะลบ ม็อด: {0}\n\nคุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อ?",
"DialogModManagerDeletionAllWarningMessage": "คุณกำลังจะลบม็อดทั้งหมดสำหรับชื่อนี้\n\nคุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อ?",
"SettingsTabGraphicsFeaturesOptions": "คุณสมบัติ",
- "SettingsTabGraphicsBackendMultithreading": "มัลติเธรด แบ็กเอนด์กราฟิก:",
+ "SettingsTabGraphicsBackendMultithreading": "มัลติเธรด กราฟิกเบื้องหลัง:",
"CommonAuto": "อัตโนมัติ",
"CommonOff": "ปิดการใช้งาน",
"CommonOn": "เปิดใช้งาน",
@@ -421,7 +527,7 @@
"AboutRyujinxAboutContent": "รียูจินซ์ เป็นอีมูเลเตอร์สำหรับ Nintendo Switch™\nโปรดสนับสนุนเราบน เพทรีออน\nรับข่าวสารล่าสุดทั้งหมดบน ทวิตเตอร์ หรือ ดิสคอร์ด ของเรา\nนักพัฒนาที่สนใจจะมีส่วนร่วมสามารถดูข้อมูลเพิ่มเติมได้ที่ กิตฮับ หรือ ดิสคอร์ด ของเรา",
"AboutRyujinxMaintainersTitle": "ได้รับการดูแลรักษาโดย:",
"AboutRyujinxMaintainersContentTooltipMessage": "คลิกเพื่อเปิดหน้าผู้ร่วมให้ข้อมูลในเบราว์เซอร์เริ่มต้นของคุณ",
- "AboutRyujinxSupprtersTitle": "สนับสนุนบน เพทรีออน โดย:",
+ "AboutRyujinxSupprtersTitle": "ลายนามผู้สนับสนุนบน เพทรีออน:",
"AmiiboSeriesLabel": "อะมิโบซีรีส์",
"AmiiboCharacterLabel": "ตัวละคร",
"AmiiboScanButtonLabel": "สแกนเลย",
@@ -451,7 +557,7 @@
"CustomThemeCheckTooltip": "ใช้ธีม Avalonia แบบกำหนดเองสำหรับ GUI เพื่อเปลี่ยนรูปลักษณ์ของเมนูโปรแกรมจำลอง",
"CustomThemePathTooltip": "ไปยังที่เก็บไฟล์ธีม GUI แบบกำหนดเอง",
"CustomThemeBrowseTooltip": "เรียกดูธีม GUI ที่กำหนดเอง",
- "DockModeToggleTooltip": "ด็อกโหมดทำให้ระบบจำลองการทำงานเสมือน Nintendo ที่กำลังเชื่อมต่ออยู่ด็อก สิ่งนี้จะปรับปรุงความเสถียรภาพของกราฟิกในเกมส่วนใหญ่ ในทางกลับกัน การปิดใช้จะทำให้ระบบจำลองทำงานเหมือนกับ Nintendo Switch แบบพกพา ส่งผลให้คุณภาพกราฟิกลดลง\n\nกำหนดค่าส่วนควบคุมของผู้เล่น 1 หากวางแผนที่จะใช้ด็อกโหมด กำหนดค่าการควบคุมแบบ แฮนด์เฮลด์ หากวางแผนที่จะใช้โหมดแฮนด์เฮลด์\n\nเปิดทิ้งไว้หากคุณไม่แน่ใจ",
+ "DockModeToggleTooltip": "ด็อกโหมด ทำให้ระบบจำลองการทำงานเสมือน Nintendo ที่กำลังเชื่อมต่ออยู่ด็อก สิ่งนี้จะปรับปรุงความเสถียรภาพของกราฟิกในเกมส่วนใหญ่ ในทางกลับกัน การปิดใช้จะทำให้ระบบจำลองทำงานเหมือนกับ Nintendo Switch แบบพกพา ส่งผลให้คุณภาพกราฟิกลดลง\n\nกำหนดค่าส่วนควบคุมของผู้เล่น 1 หากวางแผนที่จะใช้ด็อกโหมด กำหนดค่าการควบคุมแบบ แฮนด์เฮลด์ หากวางแผนที่จะใช้โหมดแฮนด์เฮลด์\n\nเปิดทิ้งไว้หากคุณไม่แน่ใจ",
"DirectKeyboardTooltip": "รองรับการเข้าถึงแป้นพิมพ์โดยตรง (HID) ให้เกมเข้าถึงคีย์บอร์ดของคุณเป็นอุปกรณ์ป้อนข้อความ\n\nใช้งานได้กับเกมที่รองรับการใช้งานคีย์บอร์ดบนฮาร์ดแวร์ของ Switch เท่านั้น\n\nหากคุณไม่แน่ใจปล่อยให้ปิดอย่างนั้น",
"DirectMouseTooltip": "รองรับการเข้าถึงเมาส์โดยตรง (HID) ให้เกมเข้าถึงเมาส์ของคุณเป็นอุปกรณ์ชี้ตำแหน่ง\n\nใช้งานได้เฉพาะกับเกมที่รองรับการควบคุมเมาส์บนฮาร์ดแวร์ของ Switch เท่านั้น ซึ่งมีอยู่ไม่มากนัก\n\nเมื่อเปิดใช้งาน ฟังก์ชั่นหน้าจอสัมผัสอาจไม่ทำงาน\n\nหากคุณไม่แน่ใจปล่อยให้ปิดอย่างนั้น",
"RegionTooltip": "เปลี่ยนภูมิภาคของระบบ",
@@ -476,27 +582,27 @@
"ResolutionScaleEntryTooltip": "สเกลความละเอียดจุดทศนิยม เช่น 1.5 ไม่ใช่จำนวนเต็มของสเกล มีแนวโน้มที่จะก่อให้เกิดปัญหาหรือความผิดพลาดได้",
"AnisotropyTooltip": "ระดับของการกรองแบบ Anisotropic ตั้งค่าเป็นอัตโนมัติเพื่อใช้ค่าที่เกมร้องขอ",
"AspectRatioTooltip": "อัตราส่วนภาพที่ใช้กับหน้าต่างตัวแสดงภาพ\n\nเปลี่ยนสิ่งนี้หากคุณใช้ตัวดัดแปลงอัตราส่วนกว้างยาวสำหรับเกมของคุณ ไม่เช่นนั้นกราฟิกจะถูกยืดออก\n\nทิ้งไว้ที่ 16:9 หากไม่แน่ใจ",
- "ShaderDumpPathTooltip": "ที่เก็บ ดัมพ์ไฟล์ของ เชเดอร์กราฟิก",
- "FileLogTooltip": "บันทึกการบันทึกคอนโซลลงในไฟล์บันทึกบนดิสก์ จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน",
- "StubLogTooltip": "พิมพ์ข้อความบันทึกต้นขั้วในคอนโซล จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน",
+ "ShaderDumpPathTooltip": "ที่เก็บ ดัมพ์ไฟล์ พื้นผิวและแสงเงา",
+ "FileLogTooltip": "บันทึก ประวัติคอนโซลลงในไฟล์บันทึกบนดิสก์ จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน",
+ "StubLogTooltip": "พิมพ์ข้อความประวัติในคอนโซล จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน",
"InfoLogTooltip": "พิมพ์ข้อความบันทึกข้อมูลในคอนโซล จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน",
- "WarnLogTooltip": "พิมพ์ข้อความบันทึกแจ้งตือนในคอนโซล จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน",
+ "WarnLogTooltip": "พิมพ์ข้อความประวัติแจ้งตือนในคอนโซล จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน",
"ErrorLogTooltip": "พิมพ์ข้อความบันทึกข้อผิดพลาดในคอนโซล จะไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน",
- "TraceLogTooltip": "พิมพ์ข้อความบันทึกการติดตามในคอนโซล ไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน",
- "GuestLogTooltip": "พิมพ์ข้อความบันทึกของผู้เยี่ยมชมในคอนโซล ไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน",
+ "TraceLogTooltip": "พิมพ์ข้อความประวัติการติดตามในคอนโซล ไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน",
+ "GuestLogTooltip": "พิมพ์ข้อความประวัติของผู้เยี่ยมชมในคอนโซล ไม่ส่งผลกระทบต่อประสิทธิภาพการทำงาน",
"FileAccessLogTooltip": "พิมพ์ข้อความบันทึกการเข้าถึงไฟล์ในคอนโซล",
- "FSAccessLogModeTooltip": "เปิดใช้งานเอาต์พุตบันทึกการเข้าถึง FS ไปยังคอนโซล โหมดที่เป็นไปได้คือ 0-3",
+ "FSAccessLogModeTooltip": "เปิดใช้งาน เอาต์พุตประวัติการเข้าถึง FS ไปยังคอนโซล โหมดที่เป็นไปได้คือ 0-3",
"DeveloperOptionTooltip": "โปรดใช้ด้วยความระมัดระวัง",
"OpenGlLogLevel": "จำเป็นต้องเปิดใช้งานระดับบันทึกที่เหมาะสม",
- "DebugLogTooltip": "พิมพ์ข้อความบันทึกการแก้ไขข้อบกพร่องในคอนโซล\n\nใช้สิ่งนี้เฉพาะเมื่อได้รับคำแนะนำจากเจ้าหน้าที่โดยเฉพาะเท่านั้น เนื่องจากจะทำให้บันทึกอ่านยากและทำให้ประสิทธิภาพของโปรแกรมจำลองแย่ลง",
+ "DebugLogTooltip": "พิมพ์ข้อความประวัติการแก้ไขข้อบกพร่องในคอนโซล\n\nใช้สิ่งนี้เฉพาะเมื่อได้รับคำแนะนำจากเจ้าหน้าที่โดยเฉพาะเท่านั้น เนื่องจากจะทำให้บันทึกอ่านยากและทำให้ประสิทธิภาพของโปรแกรมจำลองแย่ลง",
"LoadApplicationFileTooltip": "เปิด File Explorer เพื่อเลือกไฟล์ที่เข้ากันได้กับ Switch ที่จะโหลด",
"LoadApplicationFolderTooltip": "เปิดตัวสำรวจไฟล์เพื่อเลือกไฟล์ที่เข้ากันได้กับ Switch ที่จะโหลด",
- "OpenRyujinxFolderTooltip": "เปิดโฟลเดอร์ระบบไฟล์ รียูจินซ์",
- "OpenRyujinxLogsTooltip": "เปิดโฟลเดอร์ที่มีการเขียนบันทึก",
+ "OpenRyujinxFolderTooltip": "เปิดโฟลเดอร์ระบบไฟล์ Ryujinx",
+ "OpenRyujinxLogsTooltip": "เปิดโฟลเดอร์ ที่เก็บไฟล์ประวัติ",
"ExitTooltip": "ออกจากโปรแกรม รียูจินซ์",
"OpenSettingsTooltip": "เปิดหน้าต่างการตั้งค่า",
"OpenProfileManagerTooltip": "เปิดหน้าต่างตัวจัดการโปรไฟล์ผู้ใช้",
- "StopEmulationTooltip": "หยุดการจำลองเกมปัจจุบันและกลับไปยังการเลือกเกม",
+ "StopEmulationTooltip": "หยุดการจำลองของเกมที่เปิดอยู่ในปัจจุบันและกลับไปยังการเลือกเกม",
"CheckUpdatesTooltip": "ตรวจสอบการอัปเดตของ รียูจินซ์",
"OpenAboutTooltip": "เปิดหน้าต่าง เกี่ยวกับ",
"GridSize": "ขนาดตาราง",
@@ -519,7 +625,7 @@
"SettingsTabNetwork": "เครือข่าย",
"SettingsTabNetworkConnection": "การเชื่อมต่อเครือข่าย",
"SettingsTabCpuCache": "ซีพียู แคช",
- "SettingsTabCpuMemory": "ซีพียูเมมโมรี่ แคช",
+ "SettingsTabCpuMemory": "โหมดซีพียู",
"DialogUpdaterFlatpakNotSupportedMessage": "โปรดอัปเดต รียูจินซ์ ผ่านช่องทาง FlatHub",
"UpdaterDisabledWarningTitle": "ปิดใช้งานการอัปเดตแล้ว!",
"ControllerSettingsRotate90": "หมุน 90 องศา ตามเข็มนาฬิกา",
@@ -527,20 +633,20 @@
"IconSizeTooltip": "เปลี่ยนขนาดของไอคอนเกม",
"MenuBarOptionsShowConsole": "แสดง คอนโซล",
"ShaderCachePurgeError": "เกิดข้อผิดพลาดในการล้างแคชเชเดอร์ {0}: {1}",
- "UserErrorNoKeys": "ไม่พบคีย์",
- "UserErrorNoFirmware": "ไม่พบเฟิร์มแวร์",
- "UserErrorFirmwareParsingFailed": "เกิดข้อผิดพลาดในการแยกวิเคราะห์เฟิร์มแวร์",
- "UserErrorApplicationNotFound": "ไม่พบแอปพลิเคชัน",
+ "UserErrorNoKeys": "ไม่พบ คีย์",
+ "UserErrorNoFirmware": "ไม่พบ เฟิร์มแวร์",
+ "UserErrorFirmwareParsingFailed": "เกิดข้อผิดพลาดในการวิเคราะห์เฟิร์มแวร์",
+ "UserErrorApplicationNotFound": "ไม่พบ แอปพลิเคชัน",
"UserErrorUnknown": "ข้อผิดพลาดที่ไม่รู้จัก",
"UserErrorUndefined": "ข้อผิดพลาดที่ไม่ได้ระบุ",
- "UserErrorNoKeysDescription": "รียูจินซ์ ไม่พบไฟล์ 'prod.keys' ของคุณ",
- "UserErrorNoFirmwareDescription": "รียูจินซ์ ไม่พบเฟิร์มแวร์ที่ติดตั้งไว้",
- "UserErrorFirmwareParsingFailedDescription": "รียูจินซ์ ไม่สามารถแยกวิเคราะห์เฟิร์มแวร์ที่ให้มาได้ ซึ่งมักมีสาเหตุมาจากคีย์ที่ล้าสมัย",
+ "UserErrorNoKeysDescription": "รียูจินซ์ ไม่พบไฟล์ 'prod.keys' ในเครื่องของคุณ",
+ "UserErrorNoFirmwareDescription": "รียูจินซ์ ไม่พบ เฟิร์มแวร์ที่ติดตั้งไว้ในเครื่องของคุณ",
+ "UserErrorFirmwareParsingFailedDescription": "รียูจินซ์ ไม่สามารถวิเคราะห์เฟิร์มแวร์ที่ให้มาได้ ซึ่งมักมีสาเหตุมาจากคีย์ที่ล้าสมัย",
"UserErrorApplicationNotFoundDescription": "รียูจินซ์ ไม่พบแอปพลิเคชันที่ถูกต้องในที่เก็บไฟล์ที่กำหนด",
- "UserErrorUnknownDescription": "เกิดข้อผิดพลาดที่ไม่รู้จักเกิดขึ้น!",
+ "UserErrorUnknownDescription": "เกิดข้อผิดพลาดที่ไม่รู้จัก!",
"UserErrorUndefinedDescription": "เกิดข้อผิดพลาดที่ไม่สามารถระบุได้! สิ่งนี้ไม่ควรเกิดขึ้น โปรดติดต่อผู้พัฒนา!",
"OpenSetupGuideMessage": "เปิดคู่มือการตั้งค่า",
- "NoUpdate": "ไม่มีการอัพเดต",
+ "NoUpdate": "ไม่มีการอัปเดต",
"TitleUpdateVersionLabel": "เวอร์ชั่น {0}",
"RyujinxInfo": "รียูจินซ์ – ข้อมูล",
"RyujinxConfirm": "รียูจินซ์ - ยืนยัน",
@@ -565,16 +671,16 @@
"Docked": "ด็อก",
"Handheld": "แฮนด์เฮลด์",
"ConnectionError": "การเชื่อมต่อล้มเหลว",
- "AboutPageDeveloperListMore": "{0} และอื่น ๆ...",
+ "AboutPageDeveloperListMore": "{0} และอื่นๆ ...",
"ApiError": "ข้อผิดพลาดของ API",
"LoadingHeading": "กำลังโหลด {0}",
"CompilingPPTC": "กำลังคอมไพล์ PTC",
- "CompilingShaders": "กำลังคอมไพล์ เชเดอร์",
+ "CompilingShaders": "กำลังคอมไพล์ พื้นผิวและแสงเงา",
"AllKeyboards": "คีย์บอร์ดทั้งหมด",
"OpenFileDialogTitle": "เลือกไฟล์ที่สนับสนุนเพื่อเปิด",
"OpenFolderDialogTitle": "เลือกโฟลเดอร์ที่มีเกมที่แตกไฟล์แล้ว",
"AllSupportedFormats": "รูปแบบที่รองรับทั้งหมด",
- "RyujinxUpdater": "อัพเดต รียูจินซ์",
+ "RyujinxUpdater": "อัปเดต รียูจินซ์",
"SettingsTabHotkeys": "ปุ่มลัดของคีย์บอร์ด",
"SettingsTabHotkeysHotkeys": "ปุ่มลัดของคีย์บอร์ด",
"SettingsTabHotkeysToggleVsyncHotkey": "สลับเป็น VSync:",
@@ -582,7 +688,7 @@
"SettingsTabHotkeysShowUiHotkey": "แสดง UI:",
"SettingsTabHotkeysPauseHotkey": "หยุดชั่วคราว:",
"SettingsTabHotkeysToggleMuteHotkey": "ปิดเสียง:",
- "ControllerMotionTitle": "การตั้งค่าการควบคุมการเคลื่อนไหว",
+ "ControllerMotionTitle": "ตั้งค่าควบคุมการเคลื่อนไหว",
"ControllerRumbleTitle": "ตั้งค่าการสั่นไหว",
"SettingsSelectThemeFileDialogTitle": "เลือกไฟล์ธีม",
"SettingsXamlThemeFile": "ไฟล์ธีมรูปแบบ XAML",
@@ -593,11 +699,12 @@
"Writable": "สามารถเขียนได้",
"SelectDlcDialogTitle": "เลือกไฟล์ DLC",
"SelectUpdateDialogTitle": "เลือกไฟล์อัพเดต",
- "SelectModDialogTitle": "เลือกไดเรกทอรี ม็อด",
+ "SelectModDialogTitle": "เลือกไดเรกทอรี Mods",
"UserProfileWindowTitle": "จัดการโปรไฟล์ผู้ใช้",
"CheatWindowTitle": "จัดการสูตรโกง",
"DlcWindowTitle": "จัดการเนื้อหาที่ดาวน์โหลดได้สำหรับ {0} ({1})",
- "UpdateWindowTitle": "จัดการการอัพเดตชื่อเรื่อง",
+ "ModWindowTitle": "Manage Mods for {0} ({1})",
+ "UpdateWindowTitle": "จัดการอัปเดตหัวข้อ",
"CheatWindowHeading": "สูตรโกงมีให้สำหรับ {0} [{1}]",
"BuildId": "รหัสบิวด์:",
"DlcWindowHeading": "{0} เนื้อหาที่สามารถดาวน์โหลดได้",
@@ -608,21 +715,21 @@
"Discard": "ละทิ้ง",
"Paused": "หยุดชั่วคราว",
"UserProfilesSetProfileImage": "ตั้งค่ารูปโปรไฟล์",
- "UserProfileEmptyNameError": "จำเป็นต้องมีการระบุชื่อ",
+ "UserProfileEmptyNameError": "จำเป็นต้องระบุชื่อ",
"UserProfileNoImageError": "จำเป็นต้องตั้งค่ารูปโปรไฟล์",
"GameUpdateWindowHeading": "จัดการอัพเดตสำหรับ {0} ({1})",
"SettingsTabHotkeysResScaleUpHotkey": "เพิ่มความละเอียด:",
"SettingsTabHotkeysResScaleDownHotkey": "ลดความละเอียด:",
"UserProfilesName": "ชื่อ:",
"UserProfilesUserId": "รหัสผู้ใช้:",
- "SettingsTabGraphicsBackend": "แบ็กเอนด์กราฟิก",
- "SettingsTabGraphicsBackendTooltip": "เลือกแบ็กเอนด์กราฟิกที่จะใช้ในโปรแกรมจำลอง\n\nโดยรวมแล้ว Vulkan นั้นดีกว่าสำหรับกราฟิกการ์ดรุ่นใหม่ทั้งหมด ตราบใดที่ไดรเวอร์ยังอัพเดทอยู่เสมอ Vulkan ยังมีคุณสมบัติการคอมไพล์เชเดอร์ที่เร็วขึ้น (ลดอาการกระตุก) ของผู้จำหน่าย GPU ทุกรายอยู่แล้ว\n\nOpenGL อาจได้รับผลลัพธ์ที่ดีกว่าบน Nvidia GPU รุ่นเก่า, AMD GPU รุ่นเก่าบน Linux หรือบน GPU ที่มี VRAM ต่ำกว่า แม้ว่าการคอมไพล์เชเดอร์จะสะดุดมากขึ้นก็ตาม\n\nตั้งค่าเป็น Vulkan หากไม่แน่ใจ ตั้งค่าเป็น OpenGL หาก GPU ของคุณไม่รองรับ Vulkan แม้จะมีไดรเวอร์กราฟิกล่าสุดก็ตาม",
- "SettingsEnableTextureRecompression": "เปิดใช้งานการบีบอัดพื้นผิวอีกครั้ง",
+ "SettingsTabGraphicsBackend": "กราฟิกเบื้องหลัง",
+ "SettingsTabGraphicsBackendTooltip": "เลือกกราฟิกเบื้องหลังที่จะใช้ในโปรแกรมจำลอง\n\nโดยรวมแล้ว Vulkan นั้นดีกว่าสำหรับกราฟิกการ์ดรุ่นใหม่ทั้งหมด ตราบใดที่ไดรเวอร์ยังอัพเดทอยู่เสมอ Vulkan ยังมีคุณสมบัติการคอมไพล์เชเดอร์ที่เร็วขึ้น (ลดอาการกระตุก) ของผู้จำหน่าย GPU ทุกราย\n\nOpenGL อาจได้รับผลลัพธ์ที่ดีกว่าบน Nvidia GPU รุ่นเก่า, AMD GPU รุ่นเก่าบน Linux หรือบน GPU ที่มี VRAM ต่ำกว่า แม้ว่าการคอมไพล์เชเดอร์ จะทำให้อาการกระตุกมากขึ้นก็ตาม\n\nตั้งค่าเป็น Vulkan หากไม่แน่ใจ ตั้งค่าเป็น OpenGL หาก GPU ของคุณไม่รองรับ Vulkan แม้จะมีไดรเวอร์กราฟิกล่าสุดก็ตาม",
+ "SettingsEnableTextureRecompression": "เปิดใช้งาน การบีบอัดพื้นผิวอีกครั้ง",
"SettingsEnableTextureRecompressionTooltip": "บีบอัดพื้นผิว ASTC เพื่อลดการใช้งาน VRAM\n\nเกมที่ใช้รูปแบบพื้นผิวนี้ ได้แก่ Astral Chain, Bayonetta 3, Fire Emblem Engage, Metroid Prime Remastered, Super Mario Bros. Wonder และ The Legend of Zelda: Tears of the Kingdom\n\nกราฟิกการ์ดที่มี 4 กิกะไบต์ VRAM หรือน้อยกว่ามีแนวโน้มที่จะให้แคชในบางจุดขณะเล่นเกมเหล่านี้\n\nเปิดใช้งานเฉพาะในกรณีที่ VRAM ของคุณใกล้หมดในเกมที่กล่าวมาข้างต้น ปล่อยให้ปิดหากไม่แน่ใจ",
"SettingsTabGraphicsPreferredGpu": "GPU ที่ต้องการ",
"SettingsTabGraphicsPreferredGpuTooltip": "เลือกกราฟิกการ์ดที่จะใช้กับแบ็กเอนด์กราฟิก Vulkan\n\nไม่ส่งผลต่อ GPU ที่ OpenGL จะใช้\n\nตั้งค่าเป็น GPU ที่ถูกตั้งค่าสถานะเป็น \"dGPU\" หากคุณไม่แน่ใจ หากไม่มีก็ปล่อยทิ้งไว้โดยไม่มีใครแตะต้องมัน",
"SettingsAppRequiredRestartMessage": "จำเป็นต้องรีสตาร์ท รียูจินซ์",
- "SettingsGpuBackendRestartMessage": "การตั้งค่ากราฟิกแบ็กเอนด์หรือ GPU ได้รับการแก้ไขแล้ว สิ่งนี้จะต้องมีการรีสตาร์ทจึงจะสามารถใช้งานได้",
+ "SettingsGpuBackendRestartMessage": "การตั้งค่ากราฟิกเบื้องหลังหรือ GPU ได้รับการแก้ไขแล้ว สิ่งนี้จะต้องมีการรีสตาร์ทจึงจะสามารถใช้งานได้",
"SettingsGpuBackendRestartSubMessage": "คุณต้องการรีสตาร์ทตอนนี้หรือไม่?",
"RyujinxUpdaterMessage": "คุณต้องการอัพเดต รียูจินซ์ เป็นเวอร์ชั่นล่าสุดหรือไม่?",
"SettingsTabHotkeysVolumeUpHotkey": "เพิ่มระดับเสียง:",
@@ -662,9 +769,9 @@
"SettingsTabNetworkInterface": "เชื่อมต่อเครือข่าย:",
"NetworkInterfaceTooltip": "อินเทอร์เฟซเครือข่ายที่ใช้สำหรับคุณสมบัติ LAN/LDN\n\nเมื่อใช้ร่วมกับ VPN หรือ XLink Kai และเกมที่รองรับ LAN สามารถใช้เพื่อปลอมการเชื่อมต่อเครือข่ายเดียวกันผ่านทางอินเทอร์เน็ต\n\nปล่อยให้เป็น ค่าเริ่มต้น หากคุณไม่แน่ใจ",
"NetworkInterfaceDefault": "ค่าเริ่มต้น",
- "PackagingShaders": "แพ็คเชเดอร์ไฟล์",
- "AboutChangelogButton": "ดูบันทึกการเปลี่ยนแปลงบน GitHub",
- "AboutChangelogButtonTooltipMessage": "คลิกเพื่อเปิดบันทึกการเปลี่ยนแปลงสำหรับเวอร์ชั่นนี้ บนเบราว์เซอร์เริ่มต้นของคุณ",
+ "PackagingShaders": "รวม Shaders เข้าด้วยกัน",
+ "AboutChangelogButton": "ดูประวัติการเปลี่ยนแปลงบน GitHub",
+ "AboutChangelogButtonTooltipMessage": "คลิกเพื่อเปิดประวัติการเปลี่ยนแปลงสำหรับเวอร์ชั่นนี้ บนเบราว์เซอร์เริ่มต้นของคุณ",
"SettingsTabNetworkMultiplayer": "ผู้เล่นหลายคน",
"MultiplayerMode": "โหมด:",
"MultiplayerModeTooltip": "เปลี่ยนโหมดผู้เล่นหลายคนของ LDN\n\nLdnMitm จะปรับเปลี่ยนฟังก์ชันการเล่นแบบไร้สาย/ภายใน จะให้เกมทำงานเหมือนกับว่าเป็น LAN ช่วยให้สามารถเชื่อมต่อภายในเครือข่ายเดียวกันกับอินสแตนซ์ Ryujinx อื่น ๆ และคอนโซล Nintendo Switch ที่ถูกแฮ็กซึ่งมีโมดูล ldn_mitm ติดตั้งอยู่\n\nผู้เล่นหลายคนต้องการให้ผู้เล่นทุกคนอยู่ในเกมเวอร์ชันเดียวกัน (เช่น Super Smash Bros. Ultimate v13.0.1 ไม่สามารถเชื่อมต่อกับ v13.0.0)\n\nปล่อยให้ปิดการใช้งานหากไม่แน่ใจ",
diff --git a/src/Ryujinx/Assets/Locales/tr_TR.json b/src/Ryujinx/Assets/Locales/tr_TR.json
index bfb5cb53a..f74baaa18 100644
--- a/src/Ryujinx/Assets/Locales/tr_TR.json
+++ b/src/Ryujinx/Assets/Locales/tr_TR.json
@@ -30,6 +30,10 @@
"MenuBarToolsManageFileTypes": "Dosya uzantılarını yönet",
"MenuBarToolsInstallFileTypes": "Dosya uzantılarını yükle",
"MenuBarToolsUninstallFileTypes": "Dosya uzantılarını kaldır",
+ "MenuBarView": "_Görüntüle",
+ "MenuBarViewWindow": "Pencere Boyutu",
+ "MenuBarViewWindow720": "720p",
+ "MenuBarViewWindow1080": "1080p",
"MenuBarHelp": "_Yardım",
"MenuBarHelpCheckForUpdates": "Güncellemeleri Denetle",
"MenuBarHelpAbout": "Hakkında",
@@ -73,7 +77,7 @@
"GameListContextMenuCreateShortcut": "Uygulama Kısayolu Oluştur",
"GameListContextMenuCreateShortcutToolTip": "Seçilmiş uygulamayı çalıştıracak bir masaüstü kısayolu oluştur",
"GameListContextMenuCreateShortcutToolTipMacOS": "Create a shortcut in macOS's Applications folder that launches the selected Application",
- "GameListContextMenuOpenModsDirectory": "Open Mods Directory",
+ "GameListContextMenuOpenModsDirectory": "Mod Dizinini Aç",
"GameListContextMenuOpenModsDirectoryToolTip": "Opens the directory which contains Application's Mods",
"GameListContextMenuOpenSdModsDirectory": "Open Atmosphere Mods Directory",
"GameListContextMenuOpenSdModsDirectoryToolTip": "Opens the alternative SD card Atmosphere directory which contains Application's Mods. Useful for mods that are packaged for real hardware.",
@@ -92,6 +96,7 @@
"SettingsTabGeneralEnableDiscordRichPresence": "Discord Zengin İçerik'i Etkinleştir",
"SettingsTabGeneralCheckUpdatesOnLaunch": "Her Açılışta Güncellemeleri Denetle",
"SettingsTabGeneralShowConfirmExitDialog": "\"Çıkışı Onayla\" Diyaloğunu Göster",
+ "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position",
"SettingsTabGeneralHideCursor": "İşaretçiyi Gizle:",
"SettingsTabGeneralHideCursorNever": "Hiçbir Zaman",
"SettingsTabGeneralHideCursorOnIdle": "Hareketsiz Durumda",
@@ -155,7 +160,7 @@
"SettingsTabGraphicsResolutionScaleNative": "Yerel (720p/1080p)",
"SettingsTabGraphicsResolutionScale2x": "2x (1440p/2160p)",
"SettingsTabGraphicsResolutionScale3x": "3x (2160p/3240p)",
- "SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p) (Not recommended)",
+ "SettingsTabGraphicsResolutionScale4x": "4x (2880p/4320p) (Tavsiye Edilmez)",
"SettingsTabGraphicsAspectRatio": "En-Boy Oranı:",
"SettingsTabGraphicsAspectRatio4x3": "4:3",
"SettingsTabGraphicsAspectRatio16x9": "16:9",
@@ -266,6 +271,107 @@
"ControllerSettingsMotionGyroDeadzone": "Gyro Ölü Bölgesi:",
"ControllerSettingsSave": "Kaydet",
"ControllerSettingsClose": "Kapat",
+ "KeyUnknown": "Unknown",
+ "KeyShiftLeft": "Sol Shift",
+ "KeyShiftRight": "Sağ Shift",
+ "KeyControlLeft": "Sol Ctrl",
+ "KeyMacControlLeft": "⌃ Sol",
+ "KeyControlRight": "Sağ Control",
+ "KeyMacControlRight": "⌃ Sağ",
+ "KeyAltLeft": "Sol Alt",
+ "KeyMacAltLeft": "⌥ Sol",
+ "KeyAltRight": "Sağ Alt",
+ "KeyMacAltRight": "⌥ Sağ",
+ "KeyWinLeft": "⊞ Sol",
+ "KeyMacWinLeft": "⌘ Sol",
+ "KeyWinRight": "⊞ Sağ",
+ "KeyMacWinRight": "⌘ Sağ",
+ "KeyMenu": "Menü",
+ "KeyUp": "Yukarı",
+ "KeyDown": "Aşağı",
+ "KeyLeft": "Sol",
+ "KeyRight": "Sağ",
+ "KeyEnter": "Enter",
+ "KeyEscape": "Esc",
+ "KeySpace": "Space",
+ "KeyTab": "Tab",
+ "KeyBackSpace": "Geri tuşu",
+ "KeyInsert": "Insert",
+ "KeyDelete": "Delete",
+ "KeyPageUp": "Page Up",
+ "KeyPageDown": "Page Down",
+ "KeyHome": "Home",
+ "KeyEnd": "End",
+ "KeyCapsLock": "Caps Lock",
+ "KeyScrollLock": "Scroll Lock",
+ "KeyPrintScreen": "Print Screen",
+ "KeyPause": "Pause",
+ "KeyNumLock": "Num Lock",
+ "KeyClear": "Clear",
+ "KeyKeypad0": "Keypad 0",
+ "KeyKeypad1": "Keypad 1",
+ "KeyKeypad2": "Keypad 2",
+ "KeyKeypad3": "Keypad 3",
+ "KeyKeypad4": "Keypad 4",
+ "KeyKeypad5": "Keypad 5",
+ "KeyKeypad6": "Keypad 6",
+ "KeyKeypad7": "Keypad 7",
+ "KeyKeypad8": "Keypad 8",
+ "KeyKeypad9": "Keypad 9",
+ "KeyKeypadDivide": "Keypad Divide",
+ "KeyKeypadMultiply": "Keypad Multiply",
+ "KeyKeypadSubtract": "Keypad Subtract",
+ "KeyKeypadAdd": "Keypad Add",
+ "KeyKeypadDecimal": "Keypad Decimal",
+ "KeyKeypadEnter": "Keypad Enter",
+ "KeyNumber0": "0",
+ "KeyNumber1": "1",
+ "KeyNumber2": "2",
+ "KeyNumber3": "3",
+ "KeyNumber4": "4",
+ "KeyNumber5": "5",
+ "KeyNumber6": "6",
+ "KeyNumber7": "7",
+ "KeyNumber8": "8",
+ "KeyNumber9": "9",
+ "KeyTilde": "~",
+ "KeyGrave": "`",
+ "KeyMinus": "-",
+ "KeyPlus": "+",
+ "KeyBracketLeft": "[",
+ "KeyBracketRight": "]",
+ "KeySemicolon": ";",
+ "KeyQuote": "\"",
+ "KeyComma": ",",
+ "KeyPeriod": ".",
+ "KeySlash": "/",
+ "KeyBackSlash": "\\",
+ "KeyUnbound": "Unbound",
+ "GamepadLeftStick": "L Stick Button",
+ "GamepadRightStick": "R Stick Button",
+ "GamepadLeftShoulder": "Left Shoulder",
+ "GamepadRightShoulder": "Right Shoulder",
+ "GamepadLeftTrigger": "Left Trigger",
+ "GamepadRightTrigger": "Right Trigger",
+ "GamepadDpadUp": "Up",
+ "GamepadDpadDown": "Down",
+ "GamepadDpadLeft": "Left",
+ "GamepadDpadRight": "Sağ",
+ "GamepadMinus": "-",
+ "GamepadPlus": "4",
+ "GamepadGuide": "Rehber",
+ "GamepadMisc1": "Diğer",
+ "GamepadPaddle1": "Pedal 1",
+ "GamepadPaddle2": "Pedal 2",
+ "GamepadPaddle3": "Pedal 3",
+ "GamepadPaddle4": "Pedal 4",
+ "GamepadTouchpad": "Touchpad",
+ "GamepadSingleLeftTrigger0": "Sol Tetik 0",
+ "GamepadSingleRightTrigger0": "Sağ Tetik 0",
+ "GamepadSingleLeftTrigger1": "Sol Tetik 1",
+ "GamepadSingleRightTrigger1": "Sağ Tetik 1",
+ "StickLeft": "Sol Çubuk",
+ "StickRight": "Sağ çubuk",
"UserProfilesSelectedUserProfile": "Seçili Kullanıcı Profili:",
"UserProfilesSaveProfileName": "Profil İsmini Kaydet",
"UserProfilesChangeProfileImage": "Profil Resmini Değiştir",
@@ -297,9 +403,9 @@
"GameListContextMenuRunApplication": "Uygulamayı Çalıştır",
"GameListContextMenuToggleFavorite": "Favori Ayarla",
"GameListContextMenuToggleFavoriteToolTip": "Oyunu Favorilere Ekle/Çıkar",
- "SettingsTabGeneralTheme": "Theme:",
- "SettingsTabGeneralThemeDark": "Dark",
- "SettingsTabGeneralThemeLight": "Light",
+ "SettingsTabGeneralTheme": "Tema:",
+ "SettingsTabGeneralThemeDark": "Karanlık",
+ "SettingsTabGeneralThemeLight": "Aydınlık",
"ControllerSettingsConfigureGeneral": "Ayarla",
"ControllerSettingsRumble": "Titreşim",
"ControllerSettingsRumbleStrongMultiplier": "Güçlü Titreşim Çoklayıcı",
@@ -384,10 +490,10 @@
"DialogUserProfileUnsavedChangesSubMessage": "Yaptığınız değişiklikleri iptal etmek istediğinize emin misiniz?",
"DialogControllerSettingsModifiedConfirmMessage": "Geçerli kumanda seçenekleri güncellendi.",
"DialogControllerSettingsModifiedConfirmSubMessage": "Kaydetmek istiyor musunuz?",
- "DialogLoadFileErrorMessage": "{0}. Errored File: {1}",
- "DialogModAlreadyExistsMessage": "Mod already exists",
+ "DialogLoadFileErrorMessage": "{0}. Hatalı Dosya: {1}",
+ "DialogModAlreadyExistsMessage": "Mod zaten var",
"DialogModInvalidMessage": "The specified directory does not contain a mod!",
- "DialogModDeleteNoParentMessage": "Failed to Delete: Could not find the parent directory for mod \"{0}\"!",
+ "DialogModDeleteNoParentMessage": "Silme Başarısız: \"{0}\" Modu için üst dizin bulunamadı! ",
"DialogDlcNoDlcErrorMessage": "Belirtilen dosya seçilen oyun için DLC içermiyor!",
"DialogPerformanceCheckLoggingEnabledMessage": "Sadece geliştiriler için dizayn edilen Trace Loglama seçeneği etkin.",
"DialogPerformanceCheckLoggingEnabledConfirmMessage": "En iyi performans için trace loglama'nın devre dışı bırakılması tavsiye edilir. Trace loglama seçeneğini şimdi devre dışı bırakmak ister misiniz?",
@@ -434,7 +540,7 @@
"DlcManagerRemoveAllButton": "Tümünü kaldır",
"DlcManagerEnableAllButton": "Tümünü Aktif Et",
"DlcManagerDisableAllButton": "Tümünü Devre Dışı Bırak",
- "ModManagerDeleteAllButton": "Delete All",
+ "ModManagerDeleteAllButton": "Hepsini Sil",
"MenuBarOptionsChangeLanguage": "Dili Değiştir",
"MenuBarShowFileTypes": "Dosya Uzantılarını Göster",
"CommonSort": "Sırala",
@@ -509,8 +615,8 @@
"EnableInternetAccessTooltip": "Emüle edilen uygulamanın internete bağlanmasını sağlar.\n\nLAN modu bulunan oyunlar bu seçenek ile birbirine bağlanabilir ve sistemler aynı access point'e bağlanır. Bu gerçek konsolları da kapsar.\n\nNintendo sunucularına bağlanmayı sağlaMAZ. Internete bağlanmaya çalışan baz oyunların çökmesine sebep olabilr.\n\nEmin değilseniz devre dışı bırakın.",
"GameListContextMenuManageCheatToolTip": "Hileleri yönetmeyi sağlar",
"GameListContextMenuManageCheat": "Hileleri Yönet",
- "GameListContextMenuManageModToolTip": "Manage Mods",
- "GameListContextMenuManageMod": "Manage Mods",
+ "GameListContextMenuManageModToolTip": "Modları Yönet",
+ "GameListContextMenuManageMod": "Modları Yönet",
"ControllerSettingsStickRange": "Menzil:",
"DialogStopEmulationTitle": "Ryujinx - Emülasyonu Durdur",
"DialogStopEmulationMessage": "Emülasyonu durdurmak istediğinizden emin misiniz?",
@@ -593,15 +699,16 @@
"Writable": "Yazılabilir",
"SelectDlcDialogTitle": "DLC dosyalarını seç",
"SelectUpdateDialogTitle": "Güncelleme dosyalarını seç",
- "SelectModDialogTitle": "Select mod directory",
+ "SelectModDialogTitle": "Mod Dizinini Seç",
"UserProfileWindowTitle": "Kullanıcı Profillerini Yönet",
"CheatWindowTitle": "Oyun Hilelerini Yönet",
"DlcWindowTitle": "Oyun DLC'lerini Yönet",
+ "ModWindowTitle": "Manage Mods for {0} ({1})",
"UpdateWindowTitle": "Oyun Güncellemelerini Yönet",
"CheatWindowHeading": "{0} için Hile mevcut [{1}]",
"BuildId": "BuildId:",
"DlcWindowHeading": "{0} için DLC mevcut [{1}]",
- "ModWindowHeading": "{0} Mod(s)",
+ "ModWindowHeading": "{0} Mod(lar)",
"UserProfilesEditProfile": "Seçiliyi Düzenle",
"Cancel": "İptal",
"Save": "Kaydet",
@@ -668,6 +775,6 @@
"SettingsTabNetworkMultiplayer": "Çok Oyunculu",
"MultiplayerMode": "Mod:",
"MultiplayerModeTooltip": "Change LDN multiplayer mode.\n\nLdnMitm will modify local wireless/local play functionality in games to function as if it were LAN, allowing for local, same-network connections with other Ryujinx instances and hacked Nintendo Switch consoles that have the ldn_mitm module installed.\n\nMultiplayer requires all players to be on the same game version (i.e. Super Smash Bros. Ultimate v13.0.1 can't connect to v13.0.0).\n\nLeave DISABLED if unsure.",
- "MultiplayerModeDisabled": "Disabled",
+ "MultiplayerModeDisabled": "Devre Dışı",
"MultiplayerModeLdnMitm": "ldn_mitm"
}
diff --git a/src/Ryujinx/Assets/Locales/uk_UA.json b/src/Ryujinx/Assets/Locales/uk_UA.json
index dcf85eae9..976edfb1b 100644
--- a/src/Ryujinx/Assets/Locales/uk_UA.json
+++ b/src/Ryujinx/Assets/Locales/uk_UA.json
@@ -30,6 +30,10 @@
"MenuBarToolsManageFileTypes": "Керувати типами файлів",
"MenuBarToolsInstallFileTypes": "Установити типи файлів",
"MenuBarToolsUninstallFileTypes": "Видалити типи файлів",
+ "MenuBarView": "_View",
+ "MenuBarViewWindow": "Window Size",
+ "MenuBarViewWindow720": "720p",
+ "MenuBarViewWindow1080": "1080p",
"MenuBarHelp": "_Допомога",
"MenuBarHelpCheckForUpdates": "Перевірити оновлення",
"MenuBarHelpAbout": "Про застосунок",
@@ -92,6 +96,7 @@
"SettingsTabGeneralEnableDiscordRichPresence": "Увімкнути розширену присутність Discord",
"SettingsTabGeneralCheckUpdatesOnLaunch": "Перевіряти наявність оновлень під час запуску",
"SettingsTabGeneralShowConfirmExitDialog": "Показати діалогове вікно «Підтвердити вихід».",
+ "SettingsTabGeneralRememberWindowState": "Remember Window Size/Position",
"SettingsTabGeneralHideCursor": "Сховати вказівник:",
"SettingsTabGeneralHideCursorNever": "Ніколи",
"SettingsTabGeneralHideCursorOnIdle": "Сховати у режимі очікування",
@@ -266,6 +271,107 @@
"ControllerSettingsMotionGyroDeadzone": "Мертва зона гіроскопа:",
"ControllerSettingsSave": "Зберегти",
"ControllerSettingsClose": "Закрити",
+ "KeyUnknown": "Unknown",
+ "KeyShiftLeft": "Shift Left",
+ "KeyShiftRight": "Shift Right",
+ "KeyControlLeft": "Ctrl Left",
+ "KeyMacControlLeft": "⌃ Left",
+ "KeyControlRight": "Ctrl Right",
+ "KeyMacControlRight": "⌃ Right",
+ "KeyAltLeft": "Alt Left",
+ "KeyMacAltLeft": "⌥ Left",
+ "KeyAltRight": "Alt Right",
+ "KeyMacAltRight": "⌥ Right",
+ "KeyWinLeft": "⊞ Left",
+ "KeyMacWinLeft": "⌘ Left",
+ "KeyWinRight": "⊞ Right",
+ "KeyMacWinRight": "⌘ Right",
+ "KeyMenu": "Menu",
+ "KeyUp": "Up",
+ "KeyDown": "Down",
+ "KeyLeft": "Left",
+ "KeyRight": "Right",
+ "KeyEnter": "Enter",
+ "KeyEscape": "Escape",
+ "KeySpace": "Space",
+ "KeyTab": "Tab",
+ "KeyBackSpace": "Backspace",
+ "KeyInsert": "Insert",
+ "KeyDelete": "Delete",
+ "KeyPageUp": "Page Up",
+ "KeyPageDown": "Page Down",
+ "KeyHome": "Home",
+ "KeyEnd": "End",
+ "KeyCapsLock": "Caps Lock",
+ "KeyScrollLock": "Scroll Lock",
+ "KeyPrintScreen": "Print Screen",
+ "KeyPause": "Pause",
+ "KeyNumLock": "Num Lock",
+ "KeyClear": "Clear",
+ "KeyKeypad0": "Keypad 0",
+ "KeyKeypad1": "Keypad 1",
+ "KeyKeypad2": "Keypad 2",
+ "KeyKeypad3": "Keypad 3",
+ "KeyKeypad4": "Keypad 4",
+ "KeyKeypad5": "Keypad 5",
+ "KeyKeypad6": "Keypad 6",
+ "KeyKeypad7": "Keypad 7",
+ "KeyKeypad8": "Keypad 8",
+ "KeyKeypad9": "Keypad 9",
+ "KeyKeypadDivide": "Keypad Divide",
+ "KeyKeypadMultiply": "Keypad Multiply",
+ "KeyKeypadSubtract": "Keypad Subtract",
+ "KeyKeypadAdd": "Keypad Add",
+ "KeyKeypadDecimal": "Keypad Decimal",
+ "KeyKeypadEnter": "Keypad Enter",
+ "KeyNumber0": "0",
+ "KeyNumber1": "1",
+ "KeyNumber2": "2",
+ "KeyNumber3": "3",
+ "KeyNumber4": "4",
+ "KeyNumber5": "5",
+ "KeyNumber6": "6",
+ "KeyNumber7": "7",
+ "KeyNumber8": "8",
+ "KeyNumber9": "9",
+ "KeyTilde": "~",
+ "KeyGrave": "`",
+ "KeyMinus": "-",
+ "KeyPlus": "+",
+ "KeyBracketLeft": "[",
+ "KeyBracketRight": "]",
+ "KeySemicolon": ";",
+ "KeyQuote": "\"",
+ "KeyComma": ",",
+ "KeyPeriod": ".",
+ "KeySlash": "/",
+ "KeyBackSlash": "\\",
+ "KeyUnbound": "Unbound",
+ "GamepadLeftStick": "L Stick Button",
+ "GamepadRightStick": "R Stick Button",
+ "GamepadLeftShoulder": "Left Shoulder",
+ "GamepadRightShoulder": "Right Shoulder",
+ "GamepadLeftTrigger": "Left Trigger",
+ "GamepadRightTrigger": "Right Trigger",
+ "GamepadDpadUp": "Up",
+ "GamepadDpadDown": "Down",
+ "GamepadDpadLeft": "Left",
+ "GamepadDpadRight": "Right",
+ "GamepadMinus": "-",
+ "GamepadPlus": "+",
+ "GamepadGuide": "Guide",
+ "GamepadMisc1": "Misc",
+ "GamepadPaddle1": "Paddle 1",
+ "GamepadPaddle2": "Paddle 2",
+ "GamepadPaddle3": "Paddle 3",
+ "GamepadPaddle4": "Paddle 4",
+ "GamepadTouchpad": "Touchpad",
+ "GamepadSingleLeftTrigger0": "Left Trigger 0",
+ "GamepadSingleRightTrigger0": "Right Trigger 0",
+ "GamepadSingleLeftTrigger1": "Left Trigger 1",
+ "GamepadSingleRightTrigger1": "Right Trigger 1",
+ "StickLeft": "Left Stick",
+ "StickRight": "Right Stick",
"UserProfilesSelectedUserProfile": "Вибраний профіль користувача:",
"UserProfilesSaveProfileName": "Зберегти ім'я профілю",
"UserProfilesChangeProfileImage": "Змінити зображення профілю",
@@ -597,6 +703,7 @@
"UserProfileWindowTitle": "Менеджер профілів користувачів",
"CheatWindowTitle": "Менеджер читів",
"DlcWindowTitle": "Менеджер вмісту для завантаження",
+ "ModWindowTitle": "Керувати модами для {0} ({1})",
"UpdateWindowTitle": "Менеджер оновлення назв",
"CheatWindowHeading": "Коди доступні для {0} [{1}]",
"BuildId": "ID збірки:",
diff --git a/src/Ryujinx/Assets/Locales/zh_CN.json b/src/Ryujinx/Assets/Locales/zh_CN.json
index cc1d583e1..66f59ecd0 100644
--- a/src/Ryujinx/Assets/Locales/zh_CN.json
+++ b/src/Ryujinx/Assets/Locales/zh_CN.json
@@ -8,29 +8,33 @@
"SettingsTabSystemMemoryManagerModeHost": "本机映射 (较快)",
"SettingsTabSystemMemoryManagerModeHostUnchecked": "跳过检查的本机映射 (最快,但不安全)",
"SettingsTabSystemUseHypervisor": "使用 Hypervisor 虚拟化",
- "MenuBarFile": "文件",
- "MenuBarFileOpenFromFile": "加载游戏文件",
- "MenuBarFileOpenUnpacked": "加载解包后的游戏",
+ "MenuBarFile": "文件(_F)",
+ "MenuBarFileOpenFromFile": "加载游戏文件(_L)",
+ "MenuBarFileOpenUnpacked": "加载解包后的游戏(_U)",
"MenuBarFileOpenEmuFolder": "打开 Ryujinx 系统目录",
"MenuBarFileOpenLogsFolder": "打开日志目录",
- "MenuBarFileExit": "退出",
- "MenuBarOptions": "选项",
+ "MenuBarFileExit": "退出(_E)",
+ "MenuBarOptions": "选项(_O)",
"MenuBarOptionsToggleFullscreen": "切换全屏",
"MenuBarOptionsStartGamesInFullscreen": "全屏模式启动游戏",
"MenuBarOptionsStopEmulation": "停止模拟",
- "MenuBarOptionsSettings": "设置",
- "MenuBarOptionsManageUserProfiles": "管理用户账户",
- "MenuBarActions": "操作",
+ "MenuBarOptionsSettings": "设置(_S)",
+ "MenuBarOptionsManageUserProfiles": "管理用户账户(_M)",
+ "MenuBarActions": "操作(_A)",
"MenuBarOptionsSimulateWakeUpMessage": "模拟唤醒消息",
"MenuBarActionsScanAmiibo": "扫描 Amiibo",
- "MenuBarTools": "工具",
+ "MenuBarTools": "工具(_T)",
"MenuBarToolsInstallFirmware": "安装系统固件",
"MenuBarFileToolsInstallFirmwareFromFile": "从 XCI 或 ZIP 文件中安装系统固件",
"MenuBarFileToolsInstallFirmwareFromDirectory": "从文件夹中安装系统固件",
"MenuBarToolsManageFileTypes": "管理文件扩展名",
"MenuBarToolsInstallFileTypes": "关联文件扩展名",
"MenuBarToolsUninstallFileTypes": "取消关联扩展名",
- "MenuBarHelp": "帮助",
+ "MenuBarView": "视图(_V)",
+ "MenuBarViewWindow": "窗口大小",
+ "MenuBarViewWindow720": "720p",
+ "MenuBarViewWindow1080": "1080p",
+ "MenuBarHelp": "帮助(_H)",
"MenuBarHelpCheckForUpdates": "检查更新",
"MenuBarHelpAbout": "关于",
"MenuSearch": "搜索…",
@@ -92,6 +96,7 @@
"SettingsTabGeneralEnableDiscordRichPresence": "启用 Discord 在线状态展示",
"SettingsTabGeneralCheckUpdatesOnLaunch": "启动时检查更新",
"SettingsTabGeneralShowConfirmExitDialog": "退出游戏时需要确认",
+ "SettingsTabGeneralRememberWindowState": "记住窗口大小和位置",
"SettingsTabGeneralHideCursor": "隐藏鼠标指针:",
"SettingsTabGeneralHideCursorNever": "从不隐藏",
"SettingsTabGeneralHideCursorOnIdle": "自动隐藏",
@@ -266,6 +271,107 @@
"ControllerSettingsMotionGyroDeadzone": "陀螺仪死区:",
"ControllerSettingsSave": "保存",
"ControllerSettingsClose": "关闭",
+ "KeyUnknown": "未知",
+ "KeyShiftLeft": "左侧Shift",
+ "KeyShiftRight": "右侧Shift",
+ "KeyControlLeft": "左侧Ctrl",
+ "KeyMacControlLeft": "左侧⌃",
+ "KeyControlRight": "右侧Ctrl",
+ "KeyMacControlRight": "右侧⌃",
+ "KeyAltLeft": "左侧Alt",
+ "KeyMacAltLeft": "左侧⌥",
+ "KeyAltRight": "右侧Alt",
+ "KeyMacAltRight": "右侧⌥",
+ "KeyWinLeft": "左侧⊞",
+ "KeyMacWinLeft": "左侧⌘",
+ "KeyWinRight": "右侧⊞",
+ "KeyMacWinRight": "右侧⌘",
+ "KeyMenu": "菜单键",
+ "KeyUp": "上",
+ "KeyDown": "下",
+ "KeyLeft": "左",
+ "KeyRight": "右",
+ "KeyEnter": "回车键",
+ "KeyEscape": "Esc",
+ "KeySpace": "空格键",
+ "KeyTab": "Tab",
+ "KeyBackSpace": "退格键",
+ "KeyInsert": "Insert",
+ "KeyDelete": "Delete",
+ "KeyPageUp": "Page Up",
+ "KeyPageDown": "Page Down",
+ "KeyHome": "Home",
+ "KeyEnd": "End",
+ "KeyCapsLock": "Caps Lock",
+ "KeyScrollLock": "Scroll Lock",
+ "KeyPrintScreen": "Print Screen",
+ "KeyPause": "Pause",
+ "KeyNumLock": "Num Lock",
+ "KeyClear": "清除键",
+ "KeyKeypad0": "小键盘0",
+ "KeyKeypad1": "小键盘1",
+ "KeyKeypad2": "小键盘2",
+ "KeyKeypad3": "小键盘3",
+ "KeyKeypad4": "小键盘4",
+ "KeyKeypad5": "小键盘5",
+ "KeyKeypad6": "小键盘6",
+ "KeyKeypad7": "小键盘7",
+ "KeyKeypad8": "小键盘8",
+ "KeyKeypad9": "小键盘9",
+ "KeyKeypadDivide": "小键盘/",
+ "KeyKeypadMultiply": "小键盘*",
+ "KeyKeypadSubtract": "小键盘-",
+ "KeyKeypadAdd": "小键盘+",
+ "KeyKeypadDecimal": "小键盘.",
+ "KeyKeypadEnter": "小键盘回车键",
+ "KeyNumber0": "0",
+ "KeyNumber1": "1",
+ "KeyNumber2": "2",
+ "KeyNumber3": "3",
+ "KeyNumber4": "4",
+ "KeyNumber5": "5",
+ "KeyNumber6": "6",
+ "KeyNumber7": "7",
+ "KeyNumber8": "8",
+ "KeyNumber9": "9",
+ "KeyTilde": "~",
+ "KeyGrave": "`",
+ "KeyMinus": "-",
+ "KeyPlus": "+",
+ "KeyBracketLeft": "[",
+ "KeyBracketRight": "]",
+ "KeySemicolon": ";",
+ "KeyQuote": "\"",
+ "KeyComma": ",",
+ "KeyPeriod": ".",
+ "KeySlash": "/",
+ "KeyBackSlash": "\\",
+ "KeyUnbound": "未分配",
+ "GamepadLeftStick": "左摇杆按键",
+ "GamepadRightStick": "右摇杆按键",
+ "GamepadLeftShoulder": "左肩键L",
+ "GamepadRightShoulder": "右肩键R",
+ "GamepadLeftTrigger": "左扳机键ZL",
+ "GamepadRightTrigger": "右扳机键ZR",
+ "GamepadDpadUp": "上键",
+ "GamepadDpadDown": "下键",
+ "GamepadDpadLeft": "左键",
+ "GamepadDpadRight": "右键",
+ "GamepadMinus": "-键",
+ "GamepadPlus": "+键",
+ "GamepadGuide": "主页键",
+ "GamepadMisc1": "截图键",
+ "GamepadPaddle1": "其他按键1",
+ "GamepadPaddle2": "其他按键2",
+ "GamepadPaddle3": "其他按键3",
+ "GamepadPaddle4": "其他按键4",
+ "GamepadTouchpad": "触摸板",
+ "GamepadSingleLeftTrigger0": "左扳机0",
+ "GamepadSingleRightTrigger0": "右扳机0",
+ "GamepadSingleLeftTrigger1": "左扳机1",
+ "GamepadSingleRightTrigger1": "右扳机1",
+ "StickLeft": "左摇杆",
+ "StickRight": "右摇杆",
"UserProfilesSelectedUserProfile": "选定的用户账户:",
"UserProfilesSaveProfileName": "保存名称",
"UserProfilesChangeProfileImage": "更换头像",
@@ -597,6 +703,7 @@
"UserProfileWindowTitle": "管理用户账户",
"CheatWindowTitle": "金手指管理器",
"DlcWindowTitle": "管理 {0} ({1}) 的 DLC",
+ "ModWindowTitle": "管理 {0} ({1}) 的 MOD",
"UpdateWindowTitle": "游戏更新管理器",
"CheatWindowHeading": "适用于 {0} [{1}] 的金手指",
"BuildId": "游戏版本 ID:",
diff --git a/src/Ryujinx/Assets/Locales/zh_TW.json b/src/Ryujinx/Assets/Locales/zh_TW.json
index 301a55856..fc838d251 100644
--- a/src/Ryujinx/Assets/Locales/zh_TW.json
+++ b/src/Ryujinx/Assets/Locales/zh_TW.json
@@ -10,7 +10,7 @@
"SettingsTabSystemUseHypervisor": "使用 Hypervisor",
"MenuBarFile": "檔案(_F)",
"MenuBarFileOpenFromFile": "從檔案載入應用程式(_L)",
- "MenuBarFileOpenUnpacked": "載入解開封裝的遊戲(_U)",
+ "MenuBarFileOpenUnpacked": "載入未封裝的遊戲(_U)",
"MenuBarFileOpenEmuFolder": "開啟 Ryujinx 資料夾",
"MenuBarFileOpenLogsFolder": "開啟日誌資料夾",
"MenuBarFileExit": "結束(_E)",
@@ -30,6 +30,10 @@
"MenuBarToolsManageFileTypes": "管理檔案類型",
"MenuBarToolsInstallFileTypes": "安裝檔案類型",
"MenuBarToolsUninstallFileTypes": "移除檔案類型",
+ "MenuBarView": "檢視(_V)",
+ "MenuBarViewWindow": "視窗大小",
+ "MenuBarViewWindow720": "720p",
+ "MenuBarViewWindow1080": "1080p",
"MenuBarHelp": "說明(_H)",
"MenuBarHelpCheckForUpdates": "檢查更新",
"MenuBarHelpAbout": "關於",
@@ -92,6 +96,7 @@
"SettingsTabGeneralEnableDiscordRichPresence": "啟用 Discord 動態狀態展示",
"SettingsTabGeneralCheckUpdatesOnLaunch": "啟動時檢查更新",
"SettingsTabGeneralShowConfirmExitDialog": "顯示「確認結束」對話方塊",
+ "SettingsTabGeneralRememberWindowState": "記住視窗大小/位置",
"SettingsTabGeneralHideCursor": "隱藏滑鼠游標:",
"SettingsTabGeneralHideCursorNever": "從不",
"SettingsTabGeneralHideCursorOnIdle": "閒置時",
@@ -144,13 +149,13 @@
"SettingsTabGraphics": "圖形",
"SettingsTabGraphicsAPI": "圖形 API",
"SettingsTabGraphicsEnableShaderCache": "啟用著色器快取",
- "SettingsTabGraphicsAnisotropicFiltering": "各向異性過濾:",
+ "SettingsTabGraphicsAnisotropicFiltering": "各向異性過濾:",
"SettingsTabGraphicsAnisotropicFilteringAuto": "自動",
"SettingsTabGraphicsAnisotropicFiltering2x": "2 倍",
"SettingsTabGraphicsAnisotropicFiltering4x": "4 倍",
"SettingsTabGraphicsAnisotropicFiltering8x": "8 倍",
"SettingsTabGraphicsAnisotropicFiltering16x": "16 倍",
- "SettingsTabGraphicsResolutionScale": "解析度比例:",
+ "SettingsTabGraphicsResolutionScale": "解析度比例:",
"SettingsTabGraphicsResolutionScaleCustom": "自訂 (不建議使用)",
"SettingsTabGraphicsResolutionScaleNative": "原生 (720p/1080p)",
"SettingsTabGraphicsResolutionScale2x": "2 倍 (1440p/2160p)",
@@ -164,7 +169,7 @@
"SettingsTabGraphicsAspectRatio32x9": "32:9",
"SettingsTabGraphicsAspectRatioStretch": "拉伸以適應視窗",
"SettingsTabGraphicsDeveloperOptions": "開發者選項",
- "SettingsTabGraphicsShaderDumpPath": "圖形著色器傾印路徑:",
+ "SettingsTabGraphicsShaderDumpPath": "圖形著色器傾印路徑:",
"SettingsTabLogging": "日誌",
"SettingsTabLoggingLogging": "日誌",
"SettingsTabLoggingEnableLoggingToFile": "啟用日誌到檔案",
@@ -252,8 +257,8 @@
"ControllerSettingsLeftSR": "SR",
"ControllerSettingsRightSL": "SL",
"ControllerSettingsRightSR": "SR",
- "ControllerSettingsExtraButtonsLeft": "左按鍵",
- "ControllerSettingsExtraButtonsRight": "右按鍵",
+ "ControllerSettingsExtraButtonsLeft": "左背鍵",
+ "ControllerSettingsExtraButtonsRight": "右背鍵",
"ControllerSettingsMisc": "其他",
"ControllerSettingsTriggerThreshold": "扳機閾值:",
"ControllerSettingsMotion": "體感",
@@ -266,6 +271,107 @@
"ControllerSettingsMotionGyroDeadzone": "陀螺儀無感帶:",
"ControllerSettingsSave": "儲存",
"ControllerSettingsClose": "關閉",
+ "KeyUnknown": "未知",
+ "KeyShiftLeft": "左 Shift",
+ "KeyShiftRight": "右 Shift",
+ "KeyControlLeft": "左 Ctrl",
+ "KeyMacControlLeft": "左 ⌃",
+ "KeyControlRight": "右 Ctrl",
+ "KeyMacControlRight": "右 ⌃",
+ "KeyAltLeft": "左 Alt",
+ "KeyMacAltLeft": "左 ⌥",
+ "KeyAltRight": "右 Alt",
+ "KeyMacAltRight": "右 ⌥",
+ "KeyWinLeft": "左 ⊞",
+ "KeyMacWinLeft": "左 ⌘",
+ "KeyWinRight": "右 ⊞",
+ "KeyMacWinRight": "右 ⌘",
+ "KeyMenu": "功能表",
+ "KeyUp": "上",
+ "KeyDown": "下",
+ "KeyLeft": "左",
+ "KeyRight": "右",
+ "KeyEnter": "Enter 鍵",
+ "KeyEscape": "Esc 鍵",
+ "KeySpace": "空白鍵",
+ "KeyTab": "Tab 鍵",
+ "KeyBackSpace": "Backspace 鍵",
+ "KeyInsert": "Insert 鍵",
+ "KeyDelete": "Delete 鍵",
+ "KeyPageUp": "向上捲頁鍵",
+ "KeyPageDown": "向下捲頁鍵",
+ "KeyHome": "Home 鍵",
+ "KeyEnd": "End 鍵",
+ "KeyCapsLock": "Caps Lock 鍵",
+ "KeyScrollLock": "Scroll Lock 鍵",
+ "KeyPrintScreen": "Print Screen 鍵",
+ "KeyPause": "Pause 鍵",
+ "KeyNumLock": "Num Lock 鍵",
+ "KeyClear": "清除",
+ "KeyKeypad0": "數字鍵 0",
+ "KeyKeypad1": "數字鍵 1",
+ "KeyKeypad2": "數字鍵 2",
+ "KeyKeypad3": "數字鍵 3",
+ "KeyKeypad4": "數字鍵 4",
+ "KeyKeypad5": "數字鍵 5",
+ "KeyKeypad6": "數字鍵 6",
+ "KeyKeypad7": "數字鍵 7",
+ "KeyKeypad8": "數字鍵 8",
+ "KeyKeypad9": "數字鍵 9",
+ "KeyKeypadDivide": "數字鍵除號",
+ "KeyKeypadMultiply": "數字鍵乘號",
+ "KeyKeypadSubtract": "數字鍵減號",
+ "KeyKeypadAdd": "數字鍵加號",
+ "KeyKeypadDecimal": "數字鍵小數點",
+ "KeyKeypadEnter": "數字鍵 Enter",
+ "KeyNumber0": "0",
+ "KeyNumber1": "1",
+ "KeyNumber2": "2",
+ "KeyNumber3": "3",
+ "KeyNumber4": "4",
+ "KeyNumber5": "5",
+ "KeyNumber6": "6",
+ "KeyNumber7": "7",
+ "KeyNumber8": "8",
+ "KeyNumber9": "9",
+ "KeyTilde": "~",
+ "KeyGrave": "`",
+ "KeyMinus": "-",
+ "KeyPlus": "+",
+ "KeyBracketLeft": "[",
+ "KeyBracketRight": "]",
+ "KeySemicolon": ";",
+ "KeyQuote": "\"",
+ "KeyComma": ",",
+ "KeyPeriod": ".",
+ "KeySlash": "/",
+ "KeyBackSlash": "\\",
+ "KeyUnbound": "未分配",
+ "GamepadLeftStick": "左搖桿按鍵",
+ "GamepadRightStick": "右搖桿按鍵",
+ "GamepadLeftShoulder": "左肩鍵",
+ "GamepadRightShoulder": "右肩鍵",
+ "GamepadLeftTrigger": "左扳機",
+ "GamepadRightTrigger": "右扳機",
+ "GamepadDpadUp": "上",
+ "GamepadDpadDown": "下",
+ "GamepadDpadLeft": "左",
+ "GamepadDpadRight": "右",
+ "GamepadMinus": "-",
+ "GamepadPlus": "+",
+ "GamepadGuide": "快顯功能表鍵",
+ "GamepadMisc1": "其他按鍵",
+ "GamepadPaddle1": "其他按鍵 1",
+ "GamepadPaddle2": "其他按鍵 2",
+ "GamepadPaddle3": "其他按鍵 3",
+ "GamepadPaddle4": "其他按鍵 4",
+ "GamepadTouchpad": "觸控板",
+ "GamepadSingleLeftTrigger0": "左扳機 0",
+ "GamepadSingleRightTrigger0": "右扳機 0",
+ "GamepadSingleLeftTrigger1": "左扳機 1",
+ "GamepadSingleRightTrigger1": "右扳機 1",
+ "StickLeft": "左搖桿",
+ "StickRight": "右搖桿",
"UserProfilesSelectedUserProfile": "選取的使用者設定檔:",
"UserProfilesSaveProfileName": "儲存設定檔名稱",
"UserProfilesChangeProfileImage": "變更設定檔圖像",
@@ -490,7 +596,7 @@
"OpenGlLogLevel": "需要啟用適當的日誌等級",
"DebugLogTooltip": "在控制台中輸出偵錯日誌訊息。\n\n只有在人員特別指示的情況下才能使用,因為這會導致日誌難以閱讀,並降低模擬器效能。",
"LoadApplicationFileTooltip": "開啟檔案總管,選擇與 Switch 相容的檔案來載入",
- "LoadApplicationFolderTooltip": "開啟檔案總管,選擇與 Switch 相容且解開封裝的應用程式來載入",
+ "LoadApplicationFolderTooltip": "開啟檔案總管,選擇與 Switch 相容且未封裝的應用程式來載入",
"OpenRyujinxFolderTooltip": "開啟 Ryujinx 檔案系統資料夾",
"OpenRyujinxLogsTooltip": "開啟日誌被寫入的資料夾",
"ExitTooltip": "結束 Ryujinx",
@@ -550,7 +656,7 @@
"SwkbdMinRangeCharacters": "長度必須為 {0} 到 {1} 個字元",
"SoftwareKeyboard": "軟體鍵盤",
"SoftwareKeyboardModeNumeric": "必須是 0 到 9 或「.」",
- "SoftwareKeyboardModeAlphabet": "必須是非「中日韓字元」 (non CJK)",
+ "SoftwareKeyboardModeAlphabet": "必須是「非中日韓字元」 (non CJK)",
"SoftwareKeyboardModeASCII": "必須是 ASCII 文字",
"ControllerAppletControllers": "支援的控制器:",
"ControllerAppletPlayers": "玩家:",
@@ -572,7 +678,7 @@
"CompilingShaders": "正在編譯著色器",
"AllKeyboards": "所有鍵盤",
"OpenFileDialogTitle": "選取支援的檔案格式",
- "OpenFolderDialogTitle": "選取解開封裝遊戲的資料夾",
+ "OpenFolderDialogTitle": "選取未封裝遊戲的資料夾",
"AllSupportedFormats": "所有支援的格式",
"RyujinxUpdater": "Ryujinx 更新程式",
"SettingsTabHotkeys": "鍵盤快速鍵",
@@ -597,6 +703,7 @@
"UserProfileWindowTitle": "使用者設定檔管理員",
"CheatWindowTitle": "密技管理員",
"DlcWindowTitle": "管理 {0} 的可下載內容 ({1})",
+ "ModWindowTitle": "管理 {0} 的模組 ({1})",
"UpdateWindowTitle": "遊戲更新管理員",
"CheatWindowHeading": "可用於 {0} [{1}] 的密技",
"BuildId": "組建識別碼:",
@@ -610,7 +717,7 @@
"UserProfilesSetProfileImage": "設定設定檔圖像",
"UserProfileEmptyNameError": "名稱為必填",
"UserProfileNoImageError": "必須設定設定檔圖像",
- "GameUpdateWindowHeading": "可用於 {0} ({1}) 的更新",
+ "GameUpdateWindowHeading": "管理 {0} 的更新 ({1})",
"SettingsTabHotkeysResScaleUpHotkey": "提高解析度:",
"SettingsTabHotkeysResScaleDownHotkey": "降低解析度:",
"UserProfilesName": "名稱:",
@@ -635,7 +742,7 @@
"UserProfilesManageSaves": "管理存檔",
"DeleteUserSave": "您想刪除此遊戲的使用者存檔嗎?",
"IrreversibleActionNote": "此動作將無法復原。",
- "SaveManagerHeading": "管理 {0} 的存檔",
+ "SaveManagerHeading": "管理 {0} 的存檔 ({1})",
"SaveManagerTitle": "存檔管理員",
"Name": "名稱",
"Size": "大小",
@@ -647,9 +754,9 @@
"GraphicsAATooltip": "對遊戲繪製進行反鋸齒處理。\n\nFXAA 會模糊大部分圖像,而 SMAA 則會嘗試找出鋸齒邊緣並將其平滑化。\n\n不建議與 FSR 縮放濾鏡一起使用。\n\n此選項可在遊戲執行時透過點選下方的「套用」進行變更;您只需將設定視窗移到一旁,然後進行試驗,直到找到您喜歡的遊戲效果。\n\n如果不確定,請選擇無狀態。",
"GraphicsAALabel": "反鋸齒:",
"GraphicsScalingFilterLabel": "縮放過濾器:",
- "GraphicsScalingFilterTooltip": "選擇使用解析度縮放時套用的縮放過濾器。\n\nBilinear (雙線性) 濾鏡適用於 3D 遊戲,是一個安全的預設選項。\n\n建議像素美術遊戲使用 Nearest (最近性) 濾鏡。\n\nFSR 1.0 只是一個銳化濾鏡,不建議與 FXAA 或 SMAA 一起使用。\n\n此選項可在遊戲執行時透過點選下方的「套用」進行變更;您只需將設定視窗移到一旁,然後進行試驗,直到找到您喜歡的遊戲效果。\n\n如果不確定,請保持 Bilinear (雙線) 狀態。",
- "GraphicsScalingFilterBilinear": "Bilinear",
- "GraphicsScalingFilterNearest": "Nearest",
+ "GraphicsScalingFilterTooltip": "選擇使用解析度縮放時套用的縮放過濾器。\n\n雙線性 (Bilinear) 濾鏡適用於 3D 遊戲,是一個安全的預設選項。\n\n建議像素美術遊戲使用近鄰性 (Nearest) 濾鏡。\n\nFSR 1.0 只是一個銳化濾鏡,不建議與 FXAA 或 SMAA 一起使用。\n\n此選項可在遊戲執行時透過點選下方的「套用」進行變更;您只需將設定視窗移到一旁,然後進行試驗,直到找到您喜歡的遊戲效果。\n\n如果不確定,請保持雙線性 (Bilinear) 狀態。",
+ "GraphicsScalingFilterBilinear": "雙線性 (Bilinear)",
+ "GraphicsScalingFilterNearest": "近鄰性 (Nearest)",
"GraphicsScalingFilterFsr": "FSR",
"GraphicsScalingFilterLevelLabel": "日誌等級",
"GraphicsScalingFilterLevelTooltip": "設定 FSR 1.0 銳化等級。越高越清晰。",
@@ -662,12 +769,12 @@
"SettingsTabNetworkInterface": "網路介面:",
"NetworkInterfaceTooltip": "用於 LAN/LDN 功能的網路介面。\n\n與 VPN 或 XLink Kai 以及支援區域網路的遊戲配合使用,可用於在網路上偽造同網際網路連線。\n\n如果不確定,請保持預設狀態。",
"NetworkInterfaceDefault": "預設",
- "PackagingShaders": "著色器封裝",
+ "PackagingShaders": "封裝著色器",
"AboutChangelogButton": "在 GitHub 上檢視更新日誌",
"AboutChangelogButtonTooltipMessage": "在預設瀏覽器中開啟此版本的更新日誌。",
"SettingsTabNetworkMultiplayer": "多人遊戲",
"MultiplayerMode": "模式:",
"MultiplayerModeTooltip": "變更 LDN 多人遊戲模式。\n\nLdnMitm 將修改遊戲中的本機無線/本機遊戲功能,使其如同區域網路一樣執行,允許與其他安裝了 ldn_mitm 模組的 Ryujinx 實例和已破解的 Nintendo Switch 遊戲機進行本機同網路連線。\n\n多人遊戲要求所有玩家使用相同的遊戲版本 (例如,Super Smash Bros. Ultimate v13.0.1 無法連接 v13.0.0)。\n\n如果不確定,請保持 Disabled (停用) 狀態。",
- "MultiplayerModeDisabled": "Disabled",
+ "MultiplayerModeDisabled": "已停用",
"MultiplayerModeLdnMitm": "ldn_mitm"
}
From cdccf89e103694dcad3833d900b7858a49dae1ec Mon Sep 17 00:00:00 2001
From: gdkchan
Date: Wed, 15 May 2024 02:20:24 -0300
Subject: [PATCH 017/109] =?UTF-8?q?Revert=20"Disable=20keyboard=20controll?=
=?UTF-8?q?er=20input=20while=20swkbd=20is=20open=20(foreground)=20(#?=
=?UTF-8?q?=E2=80=A6"=20(#6805)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This reverts commit a3dc295c5f867bddb56a38f3a848ceb61ff30d32.
---
src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs | 5 -----
src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs | 3 ---
src/Ryujinx.Input/HLE/NpadManager.cs | 5 -----
src/Ryujinx.Input/IGamepadDriver.cs | 6 ------
src/Ryujinx/Input/AvaloniaKeyboard.cs | 2 +-
src/Ryujinx/Input/AvaloniaKeyboardDriver.cs | 2 +-
src/Ryujinx/UI/Applet/AvaHostUIHandler.cs | 2 --
7 files changed, 2 insertions(+), 23 deletions(-)
diff --git a/src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs b/src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs
index bd71c7933..e502254be 100644
--- a/src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs
+++ b/src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs
@@ -81,11 +81,6 @@ namespace Ryujinx.Input.GTK3
return _pressedKeys.Contains(nativeKey);
}
- public void Clear()
- {
- _pressedKeys.Clear();
- }
-
public IGamepad GetGamepad(string id)
{
if (!_keyboardIdentifers[0].Equals(id))
diff --git a/src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs b/src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs
index b3f509a09..1d918d21b 100644
--- a/src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs
+++ b/src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs
@@ -107,8 +107,6 @@ namespace Ryujinx.UI.Applet
swkbdDialog.SetInputLengthValidation(args.StringLengthMin, args.StringLengthMax);
swkbdDialog.SetInputValidation(args.KeyboardMode);
- ((MainWindow)_parent).RendererWidget.NpadManager.BlockInputUpdates();
-
if (swkbdDialog.Run() == (int)ResponseType.Ok)
{
inputText = swkbdDialog.InputEntry.Text;
@@ -130,7 +128,6 @@ namespace Ryujinx.UI.Applet
});
dialogCloseEvent.WaitOne();
- ((MainWindow)_parent).RendererWidget.NpadManager.UnblockInputUpdates();
userText = error ? null : inputText;
diff --git a/src/Ryujinx.Input/HLE/NpadManager.cs b/src/Ryujinx.Input/HLE/NpadManager.cs
index 2409ecf22..4c7bb8b7a 100644
--- a/src/Ryujinx.Input/HLE/NpadManager.cs
+++ b/src/Ryujinx.Input/HLE/NpadManager.cs
@@ -174,11 +174,6 @@ namespace Ryujinx.Input.HLE
{
lock (_lock)
{
- foreach (InputConfig inputConfig in _inputConfig)
- {
- _controllers[(int)inputConfig.PlayerIndex].GamepadDriver.Clear();
- }
-
_blockInputUpdates = false;
}
}
diff --git a/src/Ryujinx.Input/IGamepadDriver.cs b/src/Ryujinx.Input/IGamepadDriver.cs
index ff4d36993..67b01c26c 100644
--- a/src/Ryujinx.Input/IGamepadDriver.cs
+++ b/src/Ryujinx.Input/IGamepadDriver.cs
@@ -33,11 +33,5 @@ namespace Ryujinx.Input
/// The unique id of the gamepad
/// An instance of associated to the gamepad id given or null if not found
IGamepad GetGamepad(string id);
-
- ///
- /// Flush the internal state of the driver.
- ///
- /// Does nothing by default.
- void Clear() { }
}
}
diff --git a/src/Ryujinx/Input/AvaloniaKeyboard.cs b/src/Ryujinx/Input/AvaloniaKeyboard.cs
index ff88de79e..fbaaaabab 100644
--- a/src/Ryujinx/Input/AvaloniaKeyboard.cs
+++ b/src/Ryujinx/Input/AvaloniaKeyboard.cs
@@ -195,7 +195,7 @@ namespace Ryujinx.Ava.Input
public void Clear()
{
- _driver?.Clear();
+ _driver?.ResetKeys();
}
public void Dispose() { }
diff --git a/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs b/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs
index 9f87e821a..e9e71b99b 100644
--- a/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs
+++ b/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs
@@ -94,7 +94,7 @@ namespace Ryujinx.Ava.Input
return _pressedKeys.Contains(nativeKey);
}
- public void Clear()
+ public void ResetKeys()
{
_pressedKeys.Clear();
}
diff --git a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
index 4bcf8eb94..4bcc35a7a 100644
--- a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
+++ b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
@@ -122,7 +122,6 @@ namespace Ryujinx.Ava.UI.Applet
{
try
{
- _parent.ViewModel.AppHost.NpadManager.BlockInputUpdates();
var response = await SwkbdAppletDialog.ShowInputDialog(LocaleManager.Instance[LocaleKeys.SoftwareKeyboard], args);
if (response.Result == UserResult.Ok)
@@ -144,7 +143,6 @@ namespace Ryujinx.Ava.UI.Applet
});
dialogCloseEvent.WaitOne();
- _parent.ViewModel.AppHost.NpadManager.UnblockInputUpdates();
userText = error ? null : inputText;
From 3aea19460635ef9d1652a220e6806130dd6a7355 Mon Sep 17 00:00:00 2001
From: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com>
Date: Wed, 15 May 2024 06:06:58 -0400
Subject: [PATCH 018/109] Add Enhancement label to Feature Requests (#6804)
---
.github/ISSUE_TEMPLATE/feature_request.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index 383bbb151..399aa039c 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -1,6 +1,7 @@
name: Feature Request
description: Suggest a new feature for Ryujinx.
title: "[Feature Request]"
+labels: enhancement
body:
- type: textarea
id: overview
From 091230af222a309584ab35df8379ab3a725cd3c1 Mon Sep 17 00:00:00 2001
From: SamusAranX
Date: Wed, 15 May 2024 18:10:47 +0200
Subject: [PATCH 019/109] Improves some log messages and fixes a typo (#6773)
* Improves some log messages and fixes a typo
* oops
* Update src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs
Co-authored-by: Ac_K
* Log config file path
---------
Co-authored-by: Ac_K
Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com>
---
src/Ryujinx.Gtk3/Program.cs | 7 +++++--
src/Ryujinx.UI.Common/App/ApplicationData.cs | 2 +-
src/Ryujinx.UI.Common/App/ApplicationLibrary.cs | 2 +-
src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs | 5 ++++-
src/Ryujinx/Program.cs | 7 +++++--
5 files changed, 16 insertions(+), 7 deletions(-)
diff --git a/src/Ryujinx.Gtk3/Program.cs b/src/Ryujinx.Gtk3/Program.cs
index f69934313..749cb6978 100644
--- a/src/Ryujinx.Gtk3/Program.cs
+++ b/src/Ryujinx.Gtk3/Program.cs
@@ -181,21 +181,24 @@ namespace Ryujinx
{
// No configuration, we load the default values and save it to disk
ConfigurationPath = appDataConfigurationPath;
+ Logger.Notice.Print(LogClass.Application, $"No configuration file found. Saving default configuration to: {ConfigurationPath}");
ConfigurationState.Instance.LoadDefault();
ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath);
}
else
{
+ Logger.Notice.Print(LogClass.Application, $"Loading configuration from: {ConfigurationPath}");
+
if (ConfigurationFileFormat.TryLoad(ConfigurationPath, out ConfigurationFileFormat configurationFileFormat))
{
ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath);
}
else
{
- ConfigurationState.Instance.LoadDefault();
+ Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location: {ConfigurationPath}");
- Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location {ConfigurationPath}");
+ ConfigurationState.Instance.LoadDefault();
}
}
diff --git a/src/Ryujinx.UI.Common/App/ApplicationData.cs b/src/Ryujinx.UI.Common/App/ApplicationData.cs
index 8cc7238e9..13c05655b 100644
--- a/src/Ryujinx.UI.Common/App/ApplicationData.cs
+++ b/src/Ryujinx.UI.Common/App/ApplicationData.cs
@@ -45,7 +45,7 @@ namespace Ryujinx.UI.App.Common
if (!System.IO.Path.Exists(titleFilePath))
{
- Logger.Error?.Print(LogClass.Application, $"File does not exists. {titleFilePath}");
+ Logger.Error?.Print(LogClass.Application, $"File \"{titleFilePath}\" does not exist.");
return string.Empty;
}
diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
index 65cf7a9e6..82783e638 100644
--- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
+++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
@@ -106,7 +106,7 @@ namespace Ryujinx.UI.App.Common
if (!Directory.Exists(appDir))
{
- Logger.Warning?.Print(LogClass.Application, $"The \"game_dirs\" section in \"{ReleaseInformation.ConfigName}\" contains an invalid directory: \"{appDir}\"");
+ Logger.Warning?.Print(LogClass.Application, $"The specified game directory \"{appDir}\" does not exist.");
continue;
}
diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs
index 01c60a5e2..8420dc5d9 100644
--- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs
+++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs
@@ -11,6 +11,7 @@ using Ryujinx.UI.Common.Configuration.UI;
using Ryujinx.UI.Common.Helper;
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.Text.Json.Nodes;
namespace Ryujinx.UI.Common.Configuration
@@ -1594,7 +1595,9 @@ namespace Ryujinx.UI.Common.Configuration
private static void LogValueChange(ReactiveEventArgs eventArgs, string valueName)
{
- Ryujinx.Common.Logging.Logger.Info?.Print(LogClass.Configuration, $"{valueName} set to: {eventArgs.NewValue}");
+ string message = string.Create(CultureInfo.InvariantCulture, $"{valueName} set to: {eventArgs.NewValue}");
+
+ Ryujinx.Common.Logging.Logger.Info?.Print(LogClass.Configuration, message);
}
public static void Initialize()
diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs
index f925ce154..4f68ca24f 100644
--- a/src/Ryujinx/Program.cs
+++ b/src/Ryujinx/Program.cs
@@ -148,21 +148,24 @@ namespace Ryujinx.Ava
{
// No configuration, we load the default values and save it to disk
ConfigurationPath = appDataConfigurationPath;
+ Logger.Notice.Print(LogClass.Application, $"No configuration file found. Saving default configuration to: {ConfigurationPath}");
ConfigurationState.Instance.LoadDefault();
ConfigurationState.Instance.ToFileFormat().SaveConfig(ConfigurationPath);
}
else
{
+ Logger.Notice.Print(LogClass.Application, $"Loading configuration from: {ConfigurationPath}");
+
if (ConfigurationFileFormat.TryLoad(ConfigurationPath, out ConfigurationFileFormat configurationFileFormat))
{
ConfigurationState.Instance.Load(configurationFileFormat, ConfigurationPath);
}
else
{
- ConfigurationState.Instance.LoadDefault();
+ Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location: {ConfigurationPath}");
- Logger.Warning?.PrintMsg(LogClass.Application, $"Failed to load config! Loading the default config instead.\nFailed config location {ConfigurationPath}");
+ ConfigurationState.Instance.LoadDefault();
}
}
From 9ec8b2c01a0b00f3a33d9a23b9f5ff3758520484 Mon Sep 17 00:00:00 2001
From: MutantAura <44103205+MutantAura@users.noreply.github.com>
Date: Thu, 16 May 2024 18:19:37 +0100
Subject: [PATCH 020/109] Change Deflate compression level to `Fastest`.
(#6812)
---
src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs
index b08c44d67..c4a648fe4 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs
@@ -141,7 +141,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
if (algorithm == CompressionAlgorithm.Deflate)
{
- _activeStream = new DeflateStream(_stream, CompressionLevel.SmallestSize, true);
+ _activeStream = new DeflateStream(_stream, CompressionLevel.Fastest, true);
}
}
@@ -206,7 +206,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
stream.Write(data);
break;
case CompressionAlgorithm.Deflate:
- stream = new DeflateStream(stream, CompressionLevel.SmallestSize, true);
+ stream = new DeflateStream(stream, CompressionLevel.Fastest, true);
stream.Write(data);
stream.Dispose();
break;
From 4d84df94873a070f6f5c199438f957b24d8cf8a9 Mon Sep 17 00:00:00 2001
From: gdkchan
Date: Fri, 17 May 2024 16:46:43 -0300
Subject: [PATCH 021/109] Update audio renderer to REV12: Add support for
splitter biquad filter (#6813)
* Update audio renderer to REV12: Add support for splitter biquad filter
* Formatting
* Official names
* Update BiquadFilterState size + other fixes
* Update tests
* Update comment for version 2
* Size test for SplitterDestinationVersion2
* Should use Volume1 if no ramp
---
.../Renderer/Common/VoiceUpdateState.cs | 1 -
.../Renderer/Dsp/BiquadFilterHelper.cs | 234 ++++++-
.../Dsp/Command/BiquadFilterAndMixCommand.cs | 123 ++++
.../Renderer/Dsp/Command/CommandType.cs | 4 +-
.../Dsp/Command/MixRampGroupedCommand.cs | 16 +-
.../MultiTapBiquadFilterAndMixCommand.cs | 145 +++++
...mand.cs => MultiTapBiquadFilterCommand.cs} | 6 +-
.../Renderer/Dsp/State/BiquadFilterState.cs | 6 +-
.../ISplitterDestinationInParameter.cs | 43 ++
...SplitterDestinationInParameterVersion1.cs} | 15 +-
.../SplitterDestinationInParameterVersion2.cs | 81 +++
.../Renderer/Server/AudioRenderSystem.cs | 26 +-
.../Renderer/Server/BehaviourContext.cs | 22 +-
.../Renderer/Server/CommandBuffer.cs | 124 +++-
.../Renderer/Server/CommandGenerator.cs | 570 ++++++++++++------
.../CommandProcessingTimeEstimatorVersion1.cs | 12 +-
.../CommandProcessingTimeEstimatorVersion2.cs | 12 +-
.../CommandProcessingTimeEstimatorVersion3.cs | 12 +-
.../CommandProcessingTimeEstimatorVersion4.cs | 2 +-
.../CommandProcessingTimeEstimatorVersion5.cs | 48 ++
.../Server/ICommandProcessingTimeEstimator.cs | 4 +-
.../Renderer/Server/Mix/MixState.cs | 6 +-
.../Server/Splitter/SplitterContext.cs | 218 +++++--
.../Server/Splitter/SplitterDestination.cs | 375 +++++++++---
.../Splitter/SplitterDestinationVersion1.cs | 206 +++++++
.../Splitter/SplitterDestinationVersion2.cs | 250 ++++++++
.../Renderer/Server/Splitter/SplitterState.cs | 74 ++-
.../Renderer/Server/BehaviourContextTests.cs | 96 ++-
.../Server/SplitterDestinationTests.cs | 3 +-
29 files changed, 2342 insertions(+), 392 deletions(-)
create mode 100644 src/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterAndMixCommand.cs
create mode 100644 src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterAndMixCommand.cs
rename src/Ryujinx.Audio/Renderer/Dsp/Command/{GroupedBiquadFilterCommand.cs => MultiTapBiquadFilterCommand.cs} (84%)
create mode 100644 src/Ryujinx.Audio/Renderer/Parameter/ISplitterDestinationInParameter.cs
rename src/Ryujinx.Audio/Renderer/Parameter/{SplitterDestinationInParameter.cs => SplitterDestinationInParameterVersion1.cs} (73%)
create mode 100644 src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion2.cs
create mode 100644 src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion1.cs
create mode 100644 src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion2.cs
diff --git a/src/Ryujinx.Audio/Renderer/Common/VoiceUpdateState.cs b/src/Ryujinx.Audio/Renderer/Common/VoiceUpdateState.cs
index 608381af1..7f881373f 100644
--- a/src/Ryujinx.Audio/Renderer/Common/VoiceUpdateState.cs
+++ b/src/Ryujinx.Audio/Renderer/Common/VoiceUpdateState.cs
@@ -15,7 +15,6 @@ namespace Ryujinx.Audio.Renderer.Common
{
public const int Align = 0x10;
public const int BiquadStateOffset = 0x0;
- public const int BiquadStateSize = 0x10;
///
/// The state of the biquad filters of this voice.
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/BiquadFilterHelper.cs b/src/Ryujinx.Audio/Renderer/Dsp/BiquadFilterHelper.cs
index 1a51a1fbd..31f614d67 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/BiquadFilterHelper.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/BiquadFilterHelper.cs
@@ -16,10 +16,15 @@ namespace Ryujinx.Audio.Renderer.Dsp
/// The biquad filter parameter
/// The biquad filter state
/// The output buffer to write the result
- /// The input buffer to write the result
+ /// The input buffer to read the samples from
/// The count of samples to process
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void ProcessBiquadFilter(ref BiquadFilterParameter parameter, ref BiquadFilterState state, Span outputBuffer, ReadOnlySpan inputBuffer, uint sampleCount)
+ public static void ProcessBiquadFilter(
+ ref BiquadFilterParameter parameter,
+ ref BiquadFilterState state,
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ uint sampleCount)
{
float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter);
float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter);
@@ -40,6 +45,96 @@ namespace Ryujinx.Audio.Renderer.Dsp
}
}
+ ///
+ /// Apply a single biquad filter and mix the result into the output buffer.
+ ///
+ /// This is implemented with a direct form 1.
+ /// The biquad filter parameter
+ /// The biquad filter state
+ /// The output buffer to write the result
+ /// The input buffer to read the samples from
+ /// The count of samples to process
+ /// Mix volume
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ProcessBiquadFilterAndMix(
+ ref BiquadFilterParameter parameter,
+ ref BiquadFilterState state,
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ uint sampleCount,
+ float volume)
+ {
+ float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter);
+ float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter);
+ float a2 = FixedPointHelper.ToFloat(parameter.Numerator[2], FixedPointPrecisionForParameter);
+
+ float b1 = FixedPointHelper.ToFloat(parameter.Denominator[0], FixedPointPrecisionForParameter);
+ float b2 = FixedPointHelper.ToFloat(parameter.Denominator[1], FixedPointPrecisionForParameter);
+
+ for (int i = 0; i < sampleCount; i++)
+ {
+ float input = inputBuffer[i];
+ float output = input * a0 + state.State0 * a1 + state.State1 * a2 + state.State2 * b1 + state.State3 * b2;
+
+ state.State1 = state.State0;
+ state.State0 = input;
+ state.State3 = state.State2;
+ state.State2 = output;
+
+ outputBuffer[i] += FloatingPointHelper.MultiplyRoundUp(output, volume);
+ }
+ }
+
+ ///
+ /// Apply a single biquad filter and mix the result into the output buffer with volume ramp.
+ ///
+ /// This is implemented with a direct form 1.
+ /// The biquad filter parameter
+ /// The biquad filter state
+ /// The output buffer to write the result
+ /// The input buffer to read the samples from
+ /// The count of samples to process
+ /// Initial mix volume
+ /// Volume increment step
+ /// Last filtered sample value
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float ProcessBiquadFilterAndMixRamp(
+ ref BiquadFilterParameter parameter,
+ ref BiquadFilterState state,
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ uint sampleCount,
+ float volume,
+ float ramp)
+ {
+ float a0 = FixedPointHelper.ToFloat(parameter.Numerator[0], FixedPointPrecisionForParameter);
+ float a1 = FixedPointHelper.ToFloat(parameter.Numerator[1], FixedPointPrecisionForParameter);
+ float a2 = FixedPointHelper.ToFloat(parameter.Numerator[2], FixedPointPrecisionForParameter);
+
+ float b1 = FixedPointHelper.ToFloat(parameter.Denominator[0], FixedPointPrecisionForParameter);
+ float b2 = FixedPointHelper.ToFloat(parameter.Denominator[1], FixedPointPrecisionForParameter);
+
+ float mixState = 0f;
+
+ for (int i = 0; i < sampleCount; i++)
+ {
+ float input = inputBuffer[i];
+ float output = input * a0 + state.State0 * a1 + state.State1 * a2 + state.State2 * b1 + state.State3 * b2;
+
+ state.State1 = state.State0;
+ state.State0 = input;
+ state.State3 = state.State2;
+ state.State2 = output;
+
+ mixState = FloatingPointHelper.MultiplyRoundUp(output, volume);
+
+ outputBuffer[i] += mixState;
+ volume += ramp;
+ }
+
+ return mixState;
+ }
+
///
/// Apply multiple biquad filter.
///
@@ -47,10 +142,15 @@ namespace Ryujinx.Audio.Renderer.Dsp
/// The biquad filter parameter
/// The biquad filter state
/// The output buffer to write the result
- /// The input buffer to write the result
+ /// The input buffer to read the samples from
/// The count of samples to process
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static void ProcessBiquadFilter(ReadOnlySpan parameters, Span states, Span outputBuffer, ReadOnlySpan inputBuffer, uint sampleCount)
+ public static void ProcessBiquadFilter(
+ ReadOnlySpan parameters,
+ Span states,
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ uint sampleCount)
{
for (int stageIndex = 0; stageIndex < parameters.Length; stageIndex++)
{
@@ -67,7 +167,7 @@ namespace Ryujinx.Audio.Renderer.Dsp
for (int i = 0; i < sampleCount; i++)
{
- float input = inputBuffer[i];
+ float input = stageIndex != 0 ? outputBuffer[i] : inputBuffer[i];
float output = input * a0 + state.State0 * a1 + state.State1 * a2 + state.State2 * b1 + state.State3 * b2;
state.State1 = state.State0;
@@ -79,5 +179,129 @@ namespace Ryujinx.Audio.Renderer.Dsp
}
}
}
+
+ ///
+ /// Apply double biquad filter and mix the result into the output buffer.
+ ///
+ /// This is implemented with a direct form 1.
+ /// The biquad filter parameter
+ /// The biquad filter state
+ /// The output buffer to write the result
+ /// The input buffer to read the samples from
+ /// The count of samples to process
+ /// Mix volume
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ProcessDoubleBiquadFilterAndMix(
+ ref BiquadFilterParameter parameter0,
+ ref BiquadFilterParameter parameter1,
+ ref BiquadFilterState state0,
+ ref BiquadFilterState state1,
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ uint sampleCount,
+ float volume)
+ {
+ float a00 = FixedPointHelper.ToFloat(parameter0.Numerator[0], FixedPointPrecisionForParameter);
+ float a10 = FixedPointHelper.ToFloat(parameter0.Numerator[1], FixedPointPrecisionForParameter);
+ float a20 = FixedPointHelper.ToFloat(parameter0.Numerator[2], FixedPointPrecisionForParameter);
+
+ float b10 = FixedPointHelper.ToFloat(parameter0.Denominator[0], FixedPointPrecisionForParameter);
+ float b20 = FixedPointHelper.ToFloat(parameter0.Denominator[1], FixedPointPrecisionForParameter);
+
+ float a01 = FixedPointHelper.ToFloat(parameter1.Numerator[0], FixedPointPrecisionForParameter);
+ float a11 = FixedPointHelper.ToFloat(parameter1.Numerator[1], FixedPointPrecisionForParameter);
+ float a21 = FixedPointHelper.ToFloat(parameter1.Numerator[2], FixedPointPrecisionForParameter);
+
+ float b11 = FixedPointHelper.ToFloat(parameter1.Denominator[0], FixedPointPrecisionForParameter);
+ float b21 = FixedPointHelper.ToFloat(parameter1.Denominator[1], FixedPointPrecisionForParameter);
+
+ for (int i = 0; i < sampleCount; i++)
+ {
+ float input = inputBuffer[i];
+ float output = input * a00 + state0.State0 * a10 + state0.State1 * a20 + state0.State2 * b10 + state0.State3 * b20;
+
+ state0.State1 = state0.State0;
+ state0.State0 = input;
+ state0.State3 = state0.State2;
+ state0.State2 = output;
+
+ input = output;
+ output = input * a01 + state1.State0 * a11 + state1.State1 * a21 + state1.State2 * b11 + state1.State3 * b21;
+
+ state1.State1 = state1.State0;
+ state1.State0 = input;
+ state1.State3 = state1.State2;
+ state1.State2 = output;
+
+ outputBuffer[i] += FloatingPointHelper.MultiplyRoundUp(output, volume);
+ }
+ }
+
+ ///
+ /// Apply double biquad filter and mix the result into the output buffer with volume ramp.
+ ///
+ /// This is implemented with a direct form 1.
+ /// The biquad filter parameter
+ /// The biquad filter state
+ /// The output buffer to write the result
+ /// The input buffer to read the samples from
+ /// The count of samples to process
+ /// Initial mix volume
+ /// Volume increment step
+ /// Last filtered sample value
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static float ProcessDoubleBiquadFilterAndMixRamp(
+ ref BiquadFilterParameter parameter0,
+ ref BiquadFilterParameter parameter1,
+ ref BiquadFilterState state0,
+ ref BiquadFilterState state1,
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ uint sampleCount,
+ float volume,
+ float ramp)
+ {
+ float a00 = FixedPointHelper.ToFloat(parameter0.Numerator[0], FixedPointPrecisionForParameter);
+ float a10 = FixedPointHelper.ToFloat(parameter0.Numerator[1], FixedPointPrecisionForParameter);
+ float a20 = FixedPointHelper.ToFloat(parameter0.Numerator[2], FixedPointPrecisionForParameter);
+
+ float b10 = FixedPointHelper.ToFloat(parameter0.Denominator[0], FixedPointPrecisionForParameter);
+ float b20 = FixedPointHelper.ToFloat(parameter0.Denominator[1], FixedPointPrecisionForParameter);
+
+ float a01 = FixedPointHelper.ToFloat(parameter1.Numerator[0], FixedPointPrecisionForParameter);
+ float a11 = FixedPointHelper.ToFloat(parameter1.Numerator[1], FixedPointPrecisionForParameter);
+ float a21 = FixedPointHelper.ToFloat(parameter1.Numerator[2], FixedPointPrecisionForParameter);
+
+ float b11 = FixedPointHelper.ToFloat(parameter1.Denominator[0], FixedPointPrecisionForParameter);
+ float b21 = FixedPointHelper.ToFloat(parameter1.Denominator[1], FixedPointPrecisionForParameter);
+
+ float mixState = 0f;
+
+ for (int i = 0; i < sampleCount; i++)
+ {
+ float input = inputBuffer[i];
+ float output = input * a00 + state0.State0 * a10 + state0.State1 * a20 + state0.State2 * b10 + state0.State3 * b20;
+
+ state0.State1 = state0.State0;
+ state0.State0 = input;
+ state0.State3 = state0.State2;
+ state0.State2 = output;
+
+ input = output;
+ output = input * a01 + state1.State0 * a11 + state1.State1 * a21 + state1.State2 * b11 + state1.State3 * b21;
+
+ state1.State1 = state1.State0;
+ state1.State0 = input;
+ state1.State3 = state1.State2;
+ state1.State2 = output;
+
+ mixState = FloatingPointHelper.MultiplyRoundUp(output, volume);
+
+ outputBuffer[i] += mixState;
+ volume += ramp;
+ }
+
+ return mixState;
+ }
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterAndMixCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterAndMixCommand.cs
new file mode 100644
index 000000000..106fc0357
--- /dev/null
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/BiquadFilterAndMixCommand.cs
@@ -0,0 +1,123 @@
+using Ryujinx.Audio.Renderer.Common;
+using Ryujinx.Audio.Renderer.Dsp.State;
+using Ryujinx.Audio.Renderer.Parameter;
+using System;
+
+namespace Ryujinx.Audio.Renderer.Dsp.Command
+{
+ public class BiquadFilterAndMixCommand : ICommand
+ {
+ public bool Enabled { get; set; }
+
+ public int NodeId { get; }
+
+ public CommandType CommandType => CommandType.BiquadFilterAndMix;
+
+ public uint EstimatedProcessingTime { get; set; }
+
+ public ushort InputBufferIndex { get; }
+ public ushort OutputBufferIndex { get; }
+
+ private BiquadFilterParameter _parameter;
+
+ public Memory BiquadFilterState { get; }
+ public Memory PreviousBiquadFilterState { get; }
+
+ public Memory State { get; }
+
+ public int LastSampleIndex { get; }
+
+ public float Volume0 { get; }
+ public float Volume1 { get; }
+
+ public bool NeedInitialization { get; }
+ public bool HasVolumeRamp { get; }
+ public bool IsFirstMixBuffer { get; }
+
+ public BiquadFilterAndMixCommand(
+ float volume0,
+ float volume1,
+ uint inputBufferIndex,
+ uint outputBufferIndex,
+ int lastSampleIndex,
+ Memory state,
+ ref BiquadFilterParameter filter,
+ Memory biquadFilterState,
+ Memory previousBiquadFilterState,
+ bool needInitialization,
+ bool hasVolumeRamp,
+ bool isFirstMixBuffer,
+ int nodeId)
+ {
+ Enabled = true;
+ NodeId = nodeId;
+
+ InputBufferIndex = (ushort)inputBufferIndex;
+ OutputBufferIndex = (ushort)outputBufferIndex;
+
+ _parameter = filter;
+ BiquadFilterState = biquadFilterState;
+ PreviousBiquadFilterState = previousBiquadFilterState;
+
+ State = state;
+ LastSampleIndex = lastSampleIndex;
+
+ Volume0 = volume0;
+ Volume1 = volume1;
+
+ NeedInitialization = needInitialization;
+ HasVolumeRamp = hasVolumeRamp;
+ IsFirstMixBuffer = isFirstMixBuffer;
+ }
+
+ public void Process(CommandList context)
+ {
+ ReadOnlySpan inputBuffer = context.GetBuffer(InputBufferIndex);
+ Span outputBuffer = context.GetBuffer(OutputBufferIndex);
+
+ if (NeedInitialization)
+ {
+ // If there is no previous state, initialize to zero.
+
+ BiquadFilterState.Span[0] = new BiquadFilterState();
+ }
+ else if (IsFirstMixBuffer)
+ {
+ // This is the first buffer, set previous state to current state.
+
+ PreviousBiquadFilterState.Span[0] = BiquadFilterState.Span[0];
+ }
+ else
+ {
+ // Rewind the current state by copying back the previous state.
+
+ BiquadFilterState.Span[0] = PreviousBiquadFilterState.Span[0];
+ }
+
+ if (HasVolumeRamp)
+ {
+ float volume = Volume0;
+ float ramp = (Volume1 - Volume0) / (int)context.SampleCount;
+
+ State.Span[0].LastSamples[LastSampleIndex] = BiquadFilterHelper.ProcessBiquadFilterAndMixRamp(
+ ref _parameter,
+ ref BiquadFilterState.Span[0],
+ outputBuffer,
+ inputBuffer,
+ context.SampleCount,
+ volume,
+ ramp);
+ }
+ else
+ {
+ BiquadFilterHelper.ProcessBiquadFilterAndMix(
+ ref _parameter,
+ ref BiquadFilterState.Span[0],
+ outputBuffer,
+ inputBuffer,
+ context.SampleCount,
+ Volume1);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs
index 098a04a04..de5c0ea2c 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/CommandType.cs
@@ -30,8 +30,10 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
CopyMixBuffer,
LimiterVersion1,
LimiterVersion2,
- GroupedBiquadFilter,
+ MultiTapBiquadFilter,
CaptureBuffer,
Compressor,
+ BiquadFilterAndMix,
+ MultiTapBiquadFilterAndMix,
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/MixRampGroupedCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/MixRampGroupedCommand.cs
index 3c7dd63b2..41ac84c1a 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/MixRampGroupedCommand.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/MixRampGroupedCommand.cs
@@ -24,7 +24,14 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
public Memory State { get; }
- public MixRampGroupedCommand(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, Span volume0, Span volume1, Memory state, int nodeId)
+ public MixRampGroupedCommand(
+ uint mixBufferCount,
+ uint inputBufferIndex,
+ uint outputBufferIndex,
+ ReadOnlySpan volume0,
+ ReadOnlySpan volume1,
+ Memory state,
+ int nodeId)
{
Enabled = true;
MixBufferCount = mixBufferCount;
@@ -48,7 +55,12 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private static float ProcessMixRampGrouped(Span outputBuffer, ReadOnlySpan inputBuffer, float volume0, float volume1, int sampleCount)
+ private static float ProcessMixRampGrouped(
+ Span outputBuffer,
+ ReadOnlySpan inputBuffer,
+ float volume0,
+ float volume1,
+ int sampleCount)
{
float ramp = (volume1 - volume0) / sampleCount;
float volume = volume0;
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterAndMixCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterAndMixCommand.cs
new file mode 100644
index 000000000..e359371b4
--- /dev/null
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterAndMixCommand.cs
@@ -0,0 +1,145 @@
+using Ryujinx.Audio.Renderer.Common;
+using Ryujinx.Audio.Renderer.Dsp.State;
+using Ryujinx.Audio.Renderer.Parameter;
+using System;
+
+namespace Ryujinx.Audio.Renderer.Dsp.Command
+{
+ public class MultiTapBiquadFilterAndMixCommand : ICommand
+ {
+ public bool Enabled { get; set; }
+
+ public int NodeId { get; }
+
+ public CommandType CommandType => CommandType.MultiTapBiquadFilterAndMix;
+
+ public uint EstimatedProcessingTime { get; set; }
+
+ public ushort InputBufferIndex { get; }
+ public ushort OutputBufferIndex { get; }
+
+ private BiquadFilterParameter _parameter0;
+ private BiquadFilterParameter _parameter1;
+
+ public Memory BiquadFilterState0 { get; }
+ public Memory BiquadFilterState1 { get; }
+ public Memory PreviousBiquadFilterState0 { get; }
+ public Memory PreviousBiquadFilterState1 { get; }
+
+ public Memory State { get; }
+
+ public int LastSampleIndex { get; }
+
+ public float Volume0 { get; }
+ public float Volume1 { get; }
+
+ public bool NeedInitialization0 { get; }
+ public bool NeedInitialization1 { get; }
+ public bool HasVolumeRamp { get; }
+ public bool IsFirstMixBuffer { get; }
+
+ public MultiTapBiquadFilterAndMixCommand(
+ float volume0,
+ float volume1,
+ uint inputBufferIndex,
+ uint outputBufferIndex,
+ int lastSampleIndex,
+ Memory state,
+ ref BiquadFilterParameter filter0,
+ ref BiquadFilterParameter filter1,
+ Memory biquadFilterState0,
+ Memory biquadFilterState1,
+ Memory previousBiquadFilterState0,
+ Memory previousBiquadFilterState1,
+ bool needInitialization0,
+ bool needInitialization1,
+ bool hasVolumeRamp,
+ bool isFirstMixBuffer,
+ int nodeId)
+ {
+ Enabled = true;
+ NodeId = nodeId;
+
+ InputBufferIndex = (ushort)inputBufferIndex;
+ OutputBufferIndex = (ushort)outputBufferIndex;
+
+ _parameter0 = filter0;
+ _parameter1 = filter1;
+ BiquadFilterState0 = biquadFilterState0;
+ BiquadFilterState1 = biquadFilterState1;
+ PreviousBiquadFilterState0 = previousBiquadFilterState0;
+ PreviousBiquadFilterState1 = previousBiquadFilterState1;
+
+ State = state;
+ LastSampleIndex = lastSampleIndex;
+
+ Volume0 = volume0;
+ Volume1 = volume1;
+
+ NeedInitialization0 = needInitialization0;
+ NeedInitialization1 = needInitialization1;
+ HasVolumeRamp = hasVolumeRamp;
+ IsFirstMixBuffer = isFirstMixBuffer;
+ }
+
+ private void UpdateState(Memory state, Memory previousState, bool needInitialization)
+ {
+ if (needInitialization)
+ {
+ // If there is no previous state, initialize to zero.
+
+ state.Span[0] = new BiquadFilterState();
+ }
+ else if (IsFirstMixBuffer)
+ {
+ // This is the first buffer, set previous state to current state.
+
+ previousState.Span[0] = state.Span[0];
+ }
+ else
+ {
+ // Rewind the current state by copying back the previous state.
+
+ state.Span[0] = previousState.Span[0];
+ }
+ }
+
+ public void Process(CommandList context)
+ {
+ ReadOnlySpan inputBuffer = context.GetBuffer(InputBufferIndex);
+ Span outputBuffer = context.GetBuffer(OutputBufferIndex);
+
+ UpdateState(BiquadFilterState0, PreviousBiquadFilterState0, NeedInitialization0);
+ UpdateState(BiquadFilterState1, PreviousBiquadFilterState1, NeedInitialization1);
+
+ if (HasVolumeRamp)
+ {
+ float volume = Volume0;
+ float ramp = (Volume1 - Volume0) / (int)context.SampleCount;
+
+ State.Span[0].LastSamples[LastSampleIndex] = BiquadFilterHelper.ProcessDoubleBiquadFilterAndMixRamp(
+ ref _parameter0,
+ ref _parameter1,
+ ref BiquadFilterState0.Span[0],
+ ref BiquadFilterState1.Span[0],
+ outputBuffer,
+ inputBuffer,
+ context.SampleCount,
+ volume,
+ ramp);
+ }
+ else
+ {
+ BiquadFilterHelper.ProcessDoubleBiquadFilterAndMix(
+ ref _parameter0,
+ ref _parameter1,
+ ref BiquadFilterState0.Span[0],
+ ref BiquadFilterState1.Span[0],
+ outputBuffer,
+ inputBuffer,
+ context.SampleCount,
+ Volume1);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/Command/GroupedBiquadFilterCommand.cs b/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterCommand.cs
similarity index 84%
rename from src/Ryujinx.Audio/Renderer/Dsp/Command/GroupedBiquadFilterCommand.cs
rename to src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterCommand.cs
index 7af851bdc..e159f8ef7 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/Command/GroupedBiquadFilterCommand.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/Command/MultiTapBiquadFilterCommand.cs
@@ -4,13 +4,13 @@ using System;
namespace Ryujinx.Audio.Renderer.Dsp.Command
{
- public class GroupedBiquadFilterCommand : ICommand
+ public class MultiTapBiquadFilterCommand : ICommand
{
public bool Enabled { get; set; }
public int NodeId { get; }
- public CommandType CommandType => CommandType.GroupedBiquadFilter;
+ public CommandType CommandType => CommandType.MultiTapBiquadFilter;
public uint EstimatedProcessingTime { get; set; }
@@ -20,7 +20,7 @@ namespace Ryujinx.Audio.Renderer.Dsp.Command
private readonly int _outputBufferIndex;
private readonly bool[] _isInitialized;
- public GroupedBiquadFilterCommand(int baseIndex, ReadOnlySpan filters, Memory biquadFilterStateMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan isInitialized, int nodeId)
+ public MultiTapBiquadFilterCommand(int baseIndex, ReadOnlySpan filters, Memory biquadFilterStateMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan isInitialized, int nodeId)
{
_parameters = filters.ToArray();
_biquadFilterStates = biquadFilterStateMemory;
diff --git a/src/Ryujinx.Audio/Renderer/Dsp/State/BiquadFilterState.cs b/src/Ryujinx.Audio/Renderer/Dsp/State/BiquadFilterState.cs
index f9a32b3f9..58a2d9cce 100644
--- a/src/Ryujinx.Audio/Renderer/Dsp/State/BiquadFilterState.cs
+++ b/src/Ryujinx.Audio/Renderer/Dsp/State/BiquadFilterState.cs
@@ -2,12 +2,16 @@ using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Dsp.State
{
- [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x10)]
+ [StructLayout(LayoutKind.Sequential, Pack = 1, Size = 0x20)]
public struct BiquadFilterState
{
public float State0;
public float State1;
public float State2;
public float State3;
+ public float State4;
+ public float State5;
+ public float State6;
+ public float State7;
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Parameter/ISplitterDestinationInParameter.cs b/src/Ryujinx.Audio/Renderer/Parameter/ISplitterDestinationInParameter.cs
new file mode 100644
index 000000000..807232f20
--- /dev/null
+++ b/src/Ryujinx.Audio/Renderer/Parameter/ISplitterDestinationInParameter.cs
@@ -0,0 +1,43 @@
+using Ryujinx.Common.Memory;
+using System;
+
+namespace Ryujinx.Audio.Renderer.Parameter
+{
+ ///
+ /// Generic interface for the splitter destination parameters.
+ ///
+ public interface ISplitterDestinationInParameter
+ {
+ ///
+ /// Target splitter destination data id.
+ ///
+ int Id { get; }
+
+ ///
+ /// The mix to output the result of the splitter.
+ ///
+ int DestinationId { get; }
+
+ ///
+ /// Biquad filter parameters.
+ ///
+ Array2 BiquadFilters { get; }
+
+ ///
+ /// Set to true if in use.
+ ///
+ bool IsUsed { get; }
+
+ ///
+ /// Mix buffer volumes.
+ ///
+ /// Used when a splitter id is specified in the mix.
+ Span MixBufferVolume { get; }
+
+ ///
+ /// Check if the magic is valid.
+ ///
+ /// Returns true if the magic is valid.
+ bool IsMagicValid();
+ }
+}
diff --git a/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameter.cs b/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion1.cs
similarity index 73%
rename from src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameter.cs
rename to src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion1.cs
index b74b67be0..029c001ea 100644
--- a/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameter.cs
+++ b/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion1.cs
@@ -1,3 +1,4 @@
+using Ryujinx.Common.Memory;
using Ryujinx.Common.Utilities;
using System;
using System.Runtime.InteropServices;
@@ -5,10 +6,10 @@ using System.Runtime.InteropServices;
namespace Ryujinx.Audio.Renderer.Parameter
{
///
- /// Input header for a splitter destination update.
+ /// Input header for a splitter destination version 1 update.
///
[StructLayout(LayoutKind.Sequential, Pack = 1)]
- public struct SplitterDestinationInParameter
+ public struct SplitterDestinationInParameterVersion1 : ISplitterDestinationInParameter
{
///
/// Magic of the input header.
@@ -41,7 +42,7 @@ namespace Ryujinx.Audio.Renderer.Parameter
///
private unsafe fixed byte _reserved[3];
- [StructLayout(LayoutKind.Sequential, Size = 4 * Constants.MixBufferCountMax, Pack = 1)]
+ [StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
private struct MixArray { }
///
@@ -50,6 +51,14 @@ namespace Ryujinx.Audio.Renderer.Parameter
/// Used when a splitter id is specified in the mix.
public Span MixBufferVolume => SpanHelpers.AsSpan(ref _mixBufferVolume);
+ readonly int ISplitterDestinationInParameter.Id => Id;
+
+ readonly int ISplitterDestinationInParameter.DestinationId => DestinationId;
+
+ readonly Array2 ISplitterDestinationInParameter.BiquadFilters => default;
+
+ readonly bool ISplitterDestinationInParameter.IsUsed => IsUsed;
+
///
/// The expected constant of any input header.
///
diff --git a/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion2.cs b/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion2.cs
new file mode 100644
index 000000000..312be8b70
--- /dev/null
+++ b/src/Ryujinx.Audio/Renderer/Parameter/SplitterDestinationInParameterVersion2.cs
@@ -0,0 +1,81 @@
+using Ryujinx.Common.Memory;
+using Ryujinx.Common.Utilities;
+using System;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Audio.Renderer.Parameter
+{
+ ///
+ /// Input header for a splitter destination version 2 update.
+ ///
+ [StructLayout(LayoutKind.Sequential, Pack = 1)]
+ public struct SplitterDestinationInParameterVersion2 : ISplitterDestinationInParameter
+ {
+ ///
+ /// Magic of the input header.
+ ///
+ public uint Magic;
+
+ ///
+ /// Target splitter destination data id.
+ ///
+ public int Id;
+
+ ///
+ /// Mix buffer volumes storage.
+ ///
+ private MixArray _mixBufferVolume;
+
+ ///
+ /// The mix to output the result of the splitter.
+ ///
+ public int DestinationId;
+
+ ///
+ /// Biquad filter parameters.
+ ///
+ public Array2 BiquadFilters;
+
+ ///
+ /// Set to true if in use.
+ ///
+ [MarshalAs(UnmanagedType.I1)]
+ public bool IsUsed;
+
+ ///
+ /// Reserved/padding.
+ ///
+ private unsafe fixed byte _reserved[11];
+
+ [StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
+ private struct MixArray { }
+
+ ///
+ /// Mix buffer volumes.
+ ///
+ /// Used when a splitter id is specified in the mix.
+ public Span MixBufferVolume => SpanHelpers.AsSpan(ref _mixBufferVolume);
+
+ readonly int ISplitterDestinationInParameter.Id => Id;
+
+ readonly int ISplitterDestinationInParameter.DestinationId => DestinationId;
+
+ readonly Array2 ISplitterDestinationInParameter.BiquadFilters => BiquadFilters;
+
+ readonly bool ISplitterDestinationInParameter.IsUsed => IsUsed;
+
+ ///
+ /// The expected constant of any input header.
+ ///
+ private const uint ValidMagic = 0x44444E53;
+
+ ///
+ /// Check if the magic is valid.
+ ///
+ /// Returns true if the magic is valid.
+ public readonly bool IsMagicValid()
+ {
+ return Magic == ValidMagic;
+ }
+ }
+}
diff --git a/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs b/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs
index 9b56f5cbd..246889c48 100644
--- a/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/AudioRenderSystem.cs
@@ -1,6 +1,7 @@
using Ryujinx.Audio.Integration;
using Ryujinx.Audio.Renderer.Common;
using Ryujinx.Audio.Renderer.Dsp.Command;
+using Ryujinx.Audio.Renderer.Dsp.State;
using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Audio.Renderer.Server.Effect;
using Ryujinx.Audio.Renderer.Server.MemoryPool;
@@ -173,6 +174,22 @@ namespace Ryujinx.Audio.Renderer.Server
return ResultCode.WorkBufferTooSmall;
}
+ Memory splitterBqfStates = Memory.Empty;
+
+ if (_behaviourContext.IsBiquadFilterParameterForSplitterEnabled() &&
+ parameter.SplitterCount > 0 &&
+ parameter.SplitterDestinationCount > 0)
+ {
+ splitterBqfStates = workBufferAllocator.Allocate(parameter.SplitterDestinationCount * SplitterContext.BqfStatesPerDestination, 0x10);
+
+ if (splitterBqfStates.IsEmpty)
+ {
+ return ResultCode.WorkBufferTooSmall;
+ }
+
+ splitterBqfStates.Span.Clear();
+ }
+
// Invalidate DSP cache on what was currently allocated with workBuffer.
AudioProcessorMemoryManager.InvalidateDspCache(_dspMemoryPoolState.Translate(workBuffer, workBufferAllocator.Offset), workBufferAllocator.Offset);
@@ -292,7 +309,7 @@ namespace Ryujinx.Audio.Renderer.Server
state = MemoryPoolState.Create(MemoryPoolState.LocationType.Cpu);
}
- if (!_splitterContext.Initialize(ref _behaviourContext, ref parameter, workBufferAllocator))
+ if (!_splitterContext.Initialize(ref _behaviourContext, ref parameter, workBufferAllocator, splitterBqfStates))
{
return ResultCode.WorkBufferTooSmall;
}
@@ -775,6 +792,13 @@ namespace Ryujinx.Audio.Renderer.Server
// Splitter
size = SplitterContext.GetWorkBufferSize(size, ref behaviourContext, ref parameter);
+ if (behaviourContext.IsBiquadFilterParameterForSplitterEnabled() &&
+ parameter.SplitterCount > 0 &&
+ parameter.SplitterDestinationCount > 0)
+ {
+ size = WorkBufferAllocator.GetTargetSize(size, parameter.SplitterDestinationCount * SplitterContext.BqfStatesPerDestination, 0x10);
+ }
+
// DSP Voice
size = WorkBufferAllocator.GetTargetSize(size, parameter.VoiceCount, VoiceUpdateState.Align);
diff --git a/src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs b/src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs
index fe1dfc4be..32c7de6cf 100644
--- a/src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/BehaviourContext.cs
@@ -45,7 +45,6 @@ namespace Ryujinx.Audio.Renderer.Server
/// was added to supply the count of update done sent to the DSP.
/// A new version of the command estimator was added to address timing changes caused by the voice changes.
/// Additionally, the rendering limit percent was incremented to 80%.
- ///
///
/// This was added in system update 6.0.0
public const int Revision5 = 5 << 24;
@@ -101,10 +100,18 @@ namespace Ryujinx.Audio.Renderer.Server
/// This was added in system update 14.0.0 but some changes were made in 15.0.0
public const int Revision11 = 11 << 24;
+ ///
+ /// REV12:
+ /// Two new commands were added to for biquad filtering and mixing (with optinal volume ramp) on the same command.
+ /// Splitter destinations can now specify up to two biquad filtering parameters, used for filtering the buffer before mixing.
+ ///
+ /// This was added in system update 17.0.0
+ public const int Revision12 = 12 << 24;
+
///
/// Last revision supported by the implementation.
///
- public const int LastRevision = Revision11;
+ public const int LastRevision = Revision12;
///
/// Target revision magic supported by the implementation.
@@ -354,7 +361,7 @@ namespace Ryujinx.Audio.Renderer.Server
/// Check if the audio renderer should use an optimized Biquad Filter (Direct Form 1) in case of two biquad filters are defined on a voice.
///
/// True if the audio renderer should use the optimization.
- public bool IsBiquadFilterGroupedOptimizationSupported()
+ public bool UseMultiTapBiquadFilterProcessing()
{
return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision10);
}
@@ -368,6 +375,15 @@ namespace Ryujinx.Audio.Renderer.Server
return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision11);
}
+ ///
+ /// Check if the audio renderer should support biquad filter on splitter.
+ ///
+ /// True if the audio renderer support biquad filter on splitter
+ public bool IsBiquadFilterParameterForSplitterEnabled()
+ {
+ return CheckFeatureSupported(UserRevision, BaseRevisionMagic + Revision12);
+ }
+
///
/// Get the version of the .
///
diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs b/src/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs
index f4174a913..702f05462 100644
--- a/src/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/CommandBuffer.cs
@@ -204,7 +204,7 @@ namespace Ryujinx.Audio.Renderer.Server
}
///
- /// Create a new .
+ /// Create a new .
///
/// The base index of the input and output buffer.
/// The biquad filter parameters.
@@ -213,9 +213,9 @@ namespace Ryujinx.Audio.Renderer.Server
/// The output buffer offset.
/// Set to true if the biquad filter state is initialized.
/// The node id associated to this command.
- public void GenerateGroupedBiquadFilter(int baseIndex, ReadOnlySpan filters, Memory biquadFilterStatesMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan isInitialized, int nodeId)
+ public void GenerateMultiTapBiquadFilter(int baseIndex, ReadOnlySpan filters, Memory biquadFilterStatesMemory, int inputBufferOffset, int outputBufferOffset, ReadOnlySpan isInitialized, int nodeId)
{
- GroupedBiquadFilterCommand command = new(baseIndex, filters, biquadFilterStatesMemory, inputBufferOffset, outputBufferOffset, isInitialized, nodeId);
+ MultiTapBiquadFilterCommand command = new(baseIndex, filters, biquadFilterStatesMemory, inputBufferOffset, outputBufferOffset, isInitialized, nodeId);
command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command);
@@ -232,7 +232,7 @@ namespace Ryujinx.Audio.Renderer.Server
/// The new volume.
/// The to generate the command from.
/// The node id associated to this command.
- public void GenerateMixRampGrouped(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, Span previousVolume, Span volume, Memory state, int nodeId)
+ public void GenerateMixRampGrouped(uint mixBufferCount, uint inputBufferIndex, uint outputBufferIndex, ReadOnlySpan previousVolume, ReadOnlySpan volume, Memory state, int nodeId)
{
MixRampGroupedCommand command = new(mixBufferCount, inputBufferIndex, outputBufferIndex, previousVolume, volume, state, nodeId);
@@ -260,6 +260,120 @@ namespace Ryujinx.Audio.Renderer.Server
AddCommand(command);
}
+ ///
+ /// Generate a new .
+ ///
+ /// The previous volume.
+ /// The new volume.
+ /// The input buffer index.
+ /// The output buffer index.
+ /// The index in the array to store the ramped sample.
+ /// The to generate the command from.
+ /// The biquad filter parameter.
+ /// The biquad state.
+ /// The previous biquad state.
+ /// Set to true if the biquad filter state needs to be initialized.
+ /// Set to true if the mix has volume ramp, and should be taken into account.
+ /// Set to true if the buffer is the first mix buffer.
+ /// The node id associated to this command.
+ public void GenerateBiquadFilterAndMix(
+ float previousVolume,
+ float volume,
+ uint inputBufferIndex,
+ uint outputBufferIndex,
+ int lastSampleIndex,
+ Memory state,
+ ref BiquadFilterParameter filter,
+ Memory biquadFilterState,
+ Memory previousBiquadFilterState,
+ bool needInitialization,
+ bool hasVolumeRamp,
+ bool isFirstMixBuffer,
+ int nodeId)
+ {
+ BiquadFilterAndMixCommand command = new(
+ previousVolume,
+ volume,
+ inputBufferIndex,
+ outputBufferIndex,
+ lastSampleIndex,
+ state,
+ ref filter,
+ biquadFilterState,
+ previousBiquadFilterState,
+ needInitialization,
+ hasVolumeRamp,
+ isFirstMixBuffer,
+ nodeId);
+
+ command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command);
+
+ AddCommand(command);
+ }
+
+ ///
+ /// Generate a new .
+ ///
+ /// The previous volume.
+ /// The new volume.
+ /// The input buffer index.
+ /// The output buffer index.
+ /// The index in the array to store the ramped sample.
+ /// The to generate the command from.
+ /// First biquad filter parameter.
+ /// Second biquad filter parameter.
+ /// First biquad state.
+ /// Second biquad state.
+ /// First previous biquad state.
+ /// Second previous biquad state.
+ /// Set to true if the first biquad filter state needs to be initialized.
+ /// Set to true if the second biquad filter state needs to be initialized.
+ /// Set to true if the mix has volume ramp, and should be taken into account.
+ /// Set to true if the buffer is the first mix buffer.
+ /// The node id associated to this command.
+ public void GenerateMultiTapBiquadFilterAndMix(
+ float previousVolume,
+ float volume,
+ uint inputBufferIndex,
+ uint outputBufferIndex,
+ int lastSampleIndex,
+ Memory state,
+ ref BiquadFilterParameter filter0,
+ ref BiquadFilterParameter filter1,
+ Memory biquadFilterState0,
+ Memory biquadFilterState1,
+ Memory previousBiquadFilterState0,
+ Memory previousBiquadFilterState1,
+ bool needInitialization0,
+ bool needInitialization1,
+ bool hasVolumeRamp,
+ bool isFirstMixBuffer,
+ int nodeId)
+ {
+ MultiTapBiquadFilterAndMixCommand command = new(
+ previousVolume,
+ volume,
+ inputBufferIndex,
+ outputBufferIndex,
+ lastSampleIndex,
+ state,
+ ref filter0,
+ ref filter1,
+ biquadFilterState0,
+ biquadFilterState1,
+ previousBiquadFilterState0,
+ previousBiquadFilterState1,
+ needInitialization0,
+ needInitialization1,
+ hasVolumeRamp,
+ isFirstMixBuffer,
+ nodeId);
+
+ command.EstimatedProcessingTime = _commandProcessingTimeEstimator.Estimate(command);
+
+ AddCommand(command);
+ }
+
///
/// Generate a new .
///
@@ -268,7 +382,7 @@ namespace Ryujinx.Audio.Renderer.Server
/// The buffer count.
/// The node id associated to this command.
/// The target sample rate in use.
- public void GenerateDepopForMixBuffersCommand(Memory depopBuffer, uint bufferOffset, uint bufferCount, int nodeId, uint sampleRate)
+ public void GenerateDepopForMixBuffers(Memory depopBuffer, uint bufferOffset, uint bufferCount, int nodeId, uint sampleRate)
{
DepopForMixBuffersCommand command = new(depopBuffer, bufferOffset, bufferCount, nodeId, sampleRate);
diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs b/src/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs
index ae8f699f3..d798230c1 100644
--- a/src/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/CommandGenerator.cs
@@ -12,6 +12,7 @@ using Ryujinx.Audio.Renderer.Server.Voice;
using Ryujinx.Audio.Renderer.Utils;
using System;
using System.Diagnostics;
+using System.Runtime.CompilerServices;
namespace Ryujinx.Audio.Renderer.Server
{
@@ -46,12 +47,13 @@ namespace Ryujinx.Audio.Renderer.Server
{
ref MixState mix = ref _mixContext.GetState(voiceState.MixId);
- _commandBuffer.GenerateDepopPrepare(dspState,
- _rendererContext.DepopBuffer,
- mix.BufferCount,
- mix.BufferOffset,
- voiceState.NodeId,
- voiceState.WasPlaying);
+ _commandBuffer.GenerateDepopPrepare(
+ dspState,
+ _rendererContext.DepopBuffer,
+ mix.BufferCount,
+ mix.BufferOffset,
+ voiceState.NodeId,
+ voiceState.WasPlaying);
}
else if (voiceState.SplitterId != Constants.UnusedSplitterId)
{
@@ -59,15 +61,13 @@ namespace Ryujinx.Audio.Renderer.Server
while (true)
{
- Span destinationSpan = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId++);
+ SplitterDestination destination = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId++);
- if (destinationSpan.IsEmpty)
+ if (destination.IsNull)
{
break;
}
- ref SplitterDestination destination = ref destinationSpan[0];
-
if (destination.IsConfigured())
{
int mixId = destination.DestinationId;
@@ -76,12 +76,13 @@ namespace Ryujinx.Audio.Renderer.Server
{
ref MixState mix = ref _mixContext.GetState(mixId);
- _commandBuffer.GenerateDepopPrepare(dspState,
- _rendererContext.DepopBuffer,
- mix.BufferCount,
- mix.BufferOffset,
- voiceState.NodeId,
- voiceState.WasPlaying);
+ _commandBuffer.GenerateDepopPrepare(
+ dspState,
+ _rendererContext.DepopBuffer,
+ mix.BufferCount,
+ mix.BufferOffset,
+ voiceState.NodeId,
+ voiceState.WasPlaying);
destination.MarkAsNeedToUpdateInternalState();
}
@@ -95,35 +96,39 @@ namespace Ryujinx.Audio.Renderer.Server
if (_rendererContext.BehaviourContext.IsWaveBufferVersion2Supported())
{
- _commandBuffer.GenerateDataSourceVersion2(ref voiceState,
- dspState,
- (ushort)_rendererContext.MixBufferCount,
- (ushort)channelIndex,
- voiceState.NodeId);
+ _commandBuffer.GenerateDataSourceVersion2(
+ ref voiceState,
+ dspState,
+ (ushort)_rendererContext.MixBufferCount,
+ (ushort)channelIndex,
+ voiceState.NodeId);
}
else
{
switch (voiceState.SampleFormat)
{
case SampleFormat.PcmInt16:
- _commandBuffer.GeneratePcmInt16DataSourceVersion1(ref voiceState,
- dspState,
- (ushort)_rendererContext.MixBufferCount,
- (ushort)channelIndex,
- voiceState.NodeId);
+ _commandBuffer.GeneratePcmInt16DataSourceVersion1(
+ ref voiceState,
+ dspState,
+ (ushort)_rendererContext.MixBufferCount,
+ (ushort)channelIndex,
+ voiceState.NodeId);
break;
case SampleFormat.PcmFloat:
- _commandBuffer.GeneratePcmFloatDataSourceVersion1(ref voiceState,
- dspState,
- (ushort)_rendererContext.MixBufferCount,
- (ushort)channelIndex,
- voiceState.NodeId);
+ _commandBuffer.GeneratePcmFloatDataSourceVersion1(
+ ref voiceState,
+ dspState,
+ (ushort)_rendererContext.MixBufferCount,
+ (ushort)channelIndex,
+ voiceState.NodeId);
break;
case SampleFormat.Adpcm:
- _commandBuffer.GenerateAdpcmDataSourceVersion1(ref voiceState,
- dspState,
- (ushort)_rendererContext.MixBufferCount,
- voiceState.NodeId);
+ _commandBuffer.GenerateAdpcmDataSourceVersion1(
+ ref voiceState,
+ dspState,
+ (ushort)_rendererContext.MixBufferCount,
+ voiceState.NodeId);
break;
default:
throw new NotImplementedException($"Unsupported data source {voiceState.SampleFormat}");
@@ -134,14 +139,14 @@ namespace Ryujinx.Audio.Renderer.Server
private void GenerateBiquadFilterForVoice(ref VoiceState voiceState, Memory state, int baseIndex, int bufferOffset, int nodeId)
{
- bool supportsOptimizedPath = _rendererContext.BehaviourContext.IsBiquadFilterGroupedOptimizationSupported();
+ bool supportsOptimizedPath = _rendererContext.BehaviourContext.UseMultiTapBiquadFilterProcessing();
if (supportsOptimizedPath && voiceState.BiquadFilters[0].Enable && voiceState.BiquadFilters[1].Enable)
{
- Memory biquadStateRawMemory = SpanMemoryManager.Cast(state)[..(VoiceUpdateState.BiquadStateSize * Constants.VoiceBiquadFilterCount)];
+ Memory biquadStateRawMemory = SpanMemoryManager.Cast(state)[..(Unsafe.SizeOf() * Constants.VoiceBiquadFilterCount)];
Memory stateMemory = SpanMemoryManager.Cast(biquadStateRawMemory);
- _commandBuffer.GenerateGroupedBiquadFilter(baseIndex, voiceState.BiquadFilters.AsSpan(), stateMemory, bufferOffset, bufferOffset, voiceState.BiquadFilterNeedInitialization, nodeId);
+ _commandBuffer.GenerateMultiTapBiquadFilter(baseIndex, voiceState.BiquadFilters.AsSpan(), stateMemory, bufferOffset, bufferOffset, voiceState.BiquadFilterNeedInitialization, nodeId);
}
else
{
@@ -151,33 +156,134 @@ namespace Ryujinx.Audio.Renderer.Server
if (filter.Enable)
{
- Memory biquadStateRawMemory = SpanMemoryManager.Cast(state)[..(VoiceUpdateState.BiquadStateSize * Constants.VoiceBiquadFilterCount)];
-
+ Memory biquadStateRawMemory = SpanMemoryManager.Cast(state)[..(Unsafe.SizeOf() * Constants.VoiceBiquadFilterCount)];
Memory stateMemory = SpanMemoryManager.Cast(biquadStateRawMemory);
- _commandBuffer.GenerateBiquadFilter(baseIndex,
- ref filter,
- stateMemory.Slice(i, 1),
- bufferOffset,
- bufferOffset,
- !voiceState.BiquadFilterNeedInitialization[i],
- nodeId);
+ _commandBuffer.GenerateBiquadFilter(
+ baseIndex,
+ ref filter,
+ stateMemory.Slice(i, 1),
+ bufferOffset,
+ bufferOffset,
+ !voiceState.BiquadFilterNeedInitialization[i],
+ nodeId);
}
}
}
}
- private void GenerateVoiceMix(Span mixVolumes, Span previousMixVolumes, Memory state, uint bufferOffset, uint bufferCount, uint bufferIndex, int nodeId)
+ private void GenerateVoiceMixWithSplitter(
+ SplitterDestination destination,
+ Memory state,
+ uint bufferOffset,
+ uint bufferCount,
+ uint bufferIndex,
+ int nodeId)
+ {
+ ReadOnlySpan mixVolumes = destination.MixBufferVolume;
+ ReadOnlySpan previousMixVolumes = destination.PreviousMixBufferVolume;
+
+ ref BiquadFilterParameter bqf0 = ref destination.GetBiquadFilterParameter(0);
+ ref BiquadFilterParameter bqf1 = ref destination.GetBiquadFilterParameter(1);
+
+ Memory bqfState = _splitterContext.GetBiquadFilterState(destination);
+
+ bool isFirstMixBuffer = true;
+
+ for (int i = 0; i < bufferCount; i++)
+ {
+ float previousMixVolume = previousMixVolumes[i];
+ float mixVolume = mixVolumes[i];
+
+ if (mixVolume != 0.0f || previousMixVolume != 0.0f)
+ {
+ if (bqf0.Enable && bqf1.Enable)
+ {
+ _commandBuffer.GenerateMultiTapBiquadFilterAndMix(
+ previousMixVolume,
+ mixVolume,
+ bufferIndex,
+ bufferOffset + (uint)i,
+ i,
+ state,
+ ref bqf0,
+ ref bqf1,
+ bqfState[..1],
+ bqfState.Slice(1, 1),
+ bqfState.Slice(2, 1),
+ bqfState.Slice(3, 1),
+ !destination.IsBiquadFilterEnabledPrev(),
+ !destination.IsBiquadFilterEnabledPrev(),
+ true,
+ isFirstMixBuffer,
+ nodeId);
+
+ destination.UpdateBiquadFilterEnabledPrev(0);
+ destination.UpdateBiquadFilterEnabledPrev(1);
+ }
+ else if (bqf0.Enable)
+ {
+ _commandBuffer.GenerateBiquadFilterAndMix(
+ previousMixVolume,
+ mixVolume,
+ bufferIndex,
+ bufferOffset + (uint)i,
+ i,
+ state,
+ ref bqf0,
+ bqfState[..1],
+ bqfState.Slice(1, 1),
+ !destination.IsBiquadFilterEnabledPrev(),
+ true,
+ isFirstMixBuffer,
+ nodeId);
+
+ destination.UpdateBiquadFilterEnabledPrev(0);
+ }
+ else if (bqf1.Enable)
+ {
+ _commandBuffer.GenerateBiquadFilterAndMix(
+ previousMixVolume,
+ mixVolume,
+ bufferIndex,
+ bufferOffset + (uint)i,
+ i,
+ state,
+ ref bqf1,
+ bqfState[..1],
+ bqfState.Slice(1, 1),
+ !destination.IsBiquadFilterEnabledPrev(),
+ true,
+ isFirstMixBuffer,
+ nodeId);
+
+ destination.UpdateBiquadFilterEnabledPrev(1);
+ }
+
+ isFirstMixBuffer = false;
+ }
+ }
+ }
+
+ private void GenerateVoiceMix(
+ ReadOnlySpan mixVolumes,
+ ReadOnlySpan previousMixVolumes,
+ Memory state,
+ uint bufferOffset,
+ uint bufferCount,
+ uint bufferIndex,
+ int nodeId)
{
if (bufferCount > Constants.VoiceChannelCountMax)
{
- _commandBuffer.GenerateMixRampGrouped(bufferCount,
- bufferIndex,
- bufferOffset,
- previousMixVolumes,
- mixVolumes,
- state,
- nodeId);
+ _commandBuffer.GenerateMixRampGrouped(
+ bufferCount,
+ bufferIndex,
+ bufferOffset,
+ previousMixVolumes,
+ mixVolumes,
+ state,
+ nodeId);
}
else
{
@@ -188,13 +294,14 @@ namespace Ryujinx.Audio.Renderer.Server
if (mixVolume != 0.0f || previousMixVolume != 0.0f)
{
- _commandBuffer.GenerateMixRamp(previousMixVolume,
- mixVolume,
- bufferIndex,
- bufferOffset + (uint)i,
- i,
- state,
- nodeId);
+ _commandBuffer.GenerateMixRamp(
+ previousMixVolume,
+ mixVolume,
+ bufferIndex,
+ bufferOffset + (uint)i,
+ i,
+ state,
+ nodeId);
}
}
}
@@ -271,10 +378,11 @@ namespace Ryujinx.Audio.Renderer.Server
GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
}
- _commandBuffer.GenerateVolumeRamp(voiceState.PreviousVolume,
- voiceState.Volume,
- _rendererContext.MixBufferCount + (uint)channelIndex,
- nodeId);
+ _commandBuffer.GenerateVolumeRamp(
+ voiceState.PreviousVolume,
+ voiceState.Volume,
+ _rendererContext.MixBufferCount + (uint)channelIndex,
+ nodeId);
if (performanceInitialized)
{
@@ -291,15 +399,13 @@ namespace Ryujinx.Audio.Renderer.Server
while (true)
{
- Span destinationSpan = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId);
+ SplitterDestination destination = _splitterContext.GetDestination((int)voiceState.SplitterId, destinationId);
- if (destinationSpan.IsEmpty)
+ if (destination.IsNull)
{
break;
}
- ref SplitterDestination destination = ref destinationSpan[0];
-
destinationId += (int)channelsCount;
if (destination.IsConfigured())
@@ -310,13 +416,27 @@ namespace Ryujinx.Audio.Renderer.Server
{
ref MixState mix = ref _mixContext.GetState(mixId);
- GenerateVoiceMix(destination.MixBufferVolume,
- destination.PreviousMixBufferVolume,
- dspStateMemory,
- mix.BufferOffset,
- mix.BufferCount,
- _rendererContext.MixBufferCount + (uint)channelIndex,
- nodeId);
+ if (destination.IsBiquadFilterEnabled())
+ {
+ GenerateVoiceMixWithSplitter(
+ destination,
+ dspStateMemory,
+ mix.BufferOffset,
+ mix.BufferCount,
+ _rendererContext.MixBufferCount + (uint)channelIndex,
+ nodeId);
+ }
+ else
+ {
+ GenerateVoiceMix(
+ destination.MixBufferVolume,
+ destination.PreviousMixBufferVolume,
+ dspStateMemory,
+ mix.BufferOffset,
+ mix.BufferCount,
+ _rendererContext.MixBufferCount + (uint)channelIndex,
+ nodeId);
+ }
destination.MarkAsNeedToUpdateInternalState();
}
@@ -337,13 +457,14 @@ namespace Ryujinx.Audio.Renderer.Server
GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
}
- GenerateVoiceMix(channelResource.Mix.AsSpan(),
- channelResource.PreviousMix.AsSpan(),
- dspStateMemory,
- mix.BufferOffset,
- mix.BufferCount,
- _rendererContext.MixBufferCount + (uint)channelIndex,
- nodeId);
+ GenerateVoiceMix(
+ channelResource.Mix.AsSpan(),
+ channelResource.PreviousMix.AsSpan(),
+ dspStateMemory,
+ mix.BufferOffset,
+ mix.BufferCount,
+ _rendererContext.MixBufferCount + (uint)channelIndex,
+ nodeId);
if (performanceInitialized)
{
@@ -409,10 +530,11 @@ namespace Ryujinx.Audio.Renderer.Server
{
if (effect.Parameter.Volumes[i] != 0.0f)
{
- _commandBuffer.GenerateMix((uint)bufferOffset + effect.Parameter.Input[i],
- (uint)bufferOffset + effect.Parameter.Output[i],
- nodeId,
- effect.Parameter.Volumes[i]);
+ _commandBuffer.GenerateMix(
+ (uint)bufferOffset + effect.Parameter.Input[i],
+ (uint)bufferOffset + effect.Parameter.Output[i],
+ nodeId,
+ effect.Parameter.Volumes[i]);
}
}
}
@@ -447,17 +569,18 @@ namespace Ryujinx.Audio.Renderer.Server
updateCount = newUpdateCount;
}
- _commandBuffer.GenerateAuxEffect(bufferOffset,
- effect.Parameter.Input[i],
- effect.Parameter.Output[i],
- ref effect.State,
- effect.IsEnabled,
- effect.Parameter.BufferStorageSize,
- effect.State.SendBufferInfoBase,
- effect.State.ReturnBufferInfoBase,
- updateCount,
- writeOffset,
- nodeId);
+ _commandBuffer.GenerateAuxEffect(
+ bufferOffset,
+ effect.Parameter.Input[i],
+ effect.Parameter.Output[i],
+ ref effect.State,
+ effect.IsEnabled,
+ effect.Parameter.BufferStorageSize,
+ effect.State.SendBufferInfoBase,
+ effect.State.ReturnBufferInfoBase,
+ updateCount,
+ writeOffset,
+ nodeId);
writeOffset = newUpdateCount;
@@ -500,7 +623,7 @@ namespace Ryujinx.Audio.Renderer.Server
if (effect.IsEnabled)
{
bool needInitialization = effect.Parameter.Status == UsageState.Invalid ||
- (effect.Parameter.Status == UsageState.New && !_rendererContext.BehaviourContext.IsBiquadFilterEffectStateClearBugFixed());
+ (effect.Parameter.Status == UsageState.New && !_rendererContext.BehaviourContext.IsBiquadFilterEffectStateClearBugFixed());
BiquadFilterParameter parameter = new()
{
@@ -512,11 +635,14 @@ namespace Ryujinx.Audio.Renderer.Server
for (int i = 0; i < effect.Parameter.ChannelCount; i++)
{
- _commandBuffer.GenerateBiquadFilter((int)bufferOffset, ref parameter, effect.State.Slice(i, 1),
- effect.Parameter.Input[i],
- effect.Parameter.Output[i],
- needInitialization,
- nodeId);
+ _commandBuffer.GenerateBiquadFilter(
+ (int)bufferOffset,
+ ref parameter,
+ effect.State.Slice(i, 1),
+ effect.Parameter.Input[i],
+ effect.Parameter.Output[i],
+ needInitialization,
+ nodeId);
}
}
else
@@ -591,15 +717,16 @@ namespace Ryujinx.Audio.Renderer.Server
updateCount = newUpdateCount;
}
- _commandBuffer.GenerateCaptureEffect(bufferOffset,
- effect.Parameter.Input[i],
- effect.State.SendBufferInfo,
- effect.IsEnabled,
- effect.Parameter.BufferStorageSize,
- effect.State.SendBufferInfoBase,
- updateCount,
- writeOffset,
- nodeId);
+ _commandBuffer.GenerateCaptureEffect(
+ bufferOffset,
+ effect.Parameter.Input[i],
+ effect.State.SendBufferInfo,
+ effect.IsEnabled,
+ effect.Parameter.BufferStorageSize,
+ effect.State.SendBufferInfoBase,
+ updateCount,
+ writeOffset,
+ nodeId);
writeOffset = newUpdateCount;
@@ -612,11 +739,12 @@ namespace Ryujinx.Audio.Renderer.Server
{
Debug.Assert(effect.Type == EffectType.Compressor);
- _commandBuffer.GenerateCompressorEffect(bufferOffset,
- effect.Parameter,
- effect.State,
- effect.IsEnabled,
- nodeId);
+ _commandBuffer.GenerateCompressorEffect(
+ bufferOffset,
+ effect.Parameter,
+ effect.State,
+ effect.IsEnabled,
+ nodeId);
}
private void GenerateEffect(ref MixState mix, int effectId, BaseEffect effect)
@@ -629,8 +757,11 @@ namespace Ryujinx.Audio.Renderer.Server
bool performanceInitialized = false;
- if (_performanceManager != null && _performanceManager.GetNextEntry(out performanceEntry, effect.GetPerformanceDetailType(),
- isFinalMix ? PerformanceEntryType.FinalMix : PerformanceEntryType.SubMix, nodeId))
+ if (_performanceManager != null && _performanceManager.GetNextEntry(
+ out performanceEntry,
+ effect.GetPerformanceDetailType(),
+ isFinalMix ? PerformanceEntryType.FinalMix : PerformanceEntryType.SubMix,
+ nodeId))
{
performanceInitialized = true;
@@ -706,6 +837,85 @@ namespace Ryujinx.Audio.Renderer.Server
}
}
+ private void GenerateMixWithSplitter(
+ uint inputBufferIndex,
+ uint outputBufferIndex,
+ float volume,
+ SplitterDestination destination,
+ ref bool isFirstMixBuffer,
+ int nodeId)
+ {
+ ref BiquadFilterParameter bqf0 = ref destination.GetBiquadFilterParameter(0);
+ ref BiquadFilterParameter bqf1 = ref destination.GetBiquadFilterParameter(1);
+
+ Memory bqfState = _splitterContext.GetBiquadFilterState(destination);
+
+ if (bqf0.Enable && bqf1.Enable)
+ {
+ _commandBuffer.GenerateMultiTapBiquadFilterAndMix(
+ 0f,
+ volume,
+ inputBufferIndex,
+ outputBufferIndex,
+ 0,
+ Memory.Empty,
+ ref bqf0,
+ ref bqf1,
+ bqfState[..1],
+ bqfState.Slice(1, 1),
+ bqfState.Slice(2, 1),
+ bqfState.Slice(3, 1),
+ !destination.IsBiquadFilterEnabledPrev(),
+ !destination.IsBiquadFilterEnabledPrev(),
+ false,
+ isFirstMixBuffer,
+ nodeId);
+
+ destination.UpdateBiquadFilterEnabledPrev(0);
+ destination.UpdateBiquadFilterEnabledPrev(1);
+ }
+ else if (bqf0.Enable)
+ {
+ _commandBuffer.GenerateBiquadFilterAndMix(
+ 0f,
+ volume,
+ inputBufferIndex,
+ outputBufferIndex,
+ 0,
+ Memory.Empty,
+ ref bqf0,
+ bqfState[..1],
+ bqfState.Slice(1, 1),
+ !destination.IsBiquadFilterEnabledPrev(),
+ false,
+ isFirstMixBuffer,
+ nodeId);
+
+ destination.UpdateBiquadFilterEnabledPrev(0);
+ }
+ else if (bqf1.Enable)
+ {
+ _commandBuffer.GenerateBiquadFilterAndMix(
+ 0f,
+ volume,
+ inputBufferIndex,
+ outputBufferIndex,
+ 0,
+ Memory.Empty,
+ ref bqf1,
+ bqfState[..1],
+ bqfState.Slice(1, 1),
+ !destination.IsBiquadFilterEnabledPrev(),
+ false,
+ isFirstMixBuffer,
+ nodeId);
+
+ destination.UpdateBiquadFilterEnabledPrev(1);
+ }
+
+ isFirstMixBuffer = false;
+ }
+
private void GenerateMix(ref MixState mix)
{
if (mix.HasAnyDestination())
@@ -722,15 +932,13 @@ namespace Ryujinx.Audio.Renderer.Server
{
int destinationIndex = destinationId++;
- Span destinationSpan = _splitterContext.GetDestination((int)mix.DestinationSplitterId, destinationIndex);
+ SplitterDestination destination = _splitterContext.GetDestination((int)mix.DestinationSplitterId, destinationIndex);
- if (destinationSpan.IsEmpty)
+ if (destination.IsNull)
{
break;
}
- ref SplitterDestination destination = ref destinationSpan[0];
-
if (destination.IsConfigured())
{
int mixId = destination.DestinationId;
@@ -741,16 +949,32 @@ namespace Ryujinx.Audio.Renderer.Server
uint inputBufferIndex = mix.BufferOffset + ((uint)destinationIndex % mix.BufferCount);
+ bool isFirstMixBuffer = true;
+
for (uint bufferDestinationIndex = 0; bufferDestinationIndex < destinationMix.BufferCount; bufferDestinationIndex++)
{
float volume = mix.Volume * destination.GetMixVolume((int)bufferDestinationIndex);
if (volume != 0.0f)
{
- _commandBuffer.GenerateMix(inputBufferIndex,
- destinationMix.BufferOffset + bufferDestinationIndex,
- mix.NodeId,
- volume);
+ if (destination.IsBiquadFilterEnabled())
+ {
+ GenerateMixWithSplitter(
+ inputBufferIndex,
+ destinationMix.BufferOffset + bufferDestinationIndex,
+ volume,
+ destination,
+ ref isFirstMixBuffer,
+ mix.NodeId);
+ }
+ else
+ {
+ _commandBuffer.GenerateMix(
+ inputBufferIndex,
+ destinationMix.BufferOffset + bufferDestinationIndex,
+ mix.NodeId,
+ volume);
+ }
}
}
}
@@ -770,10 +994,11 @@ namespace Ryujinx.Audio.Renderer.Server
if (volume != 0.0f)
{
- _commandBuffer.GenerateMix(mix.BufferOffset + bufferIndex,
- destinationMix.BufferOffset + bufferDestinationIndex,
- mix.NodeId,
- volume);
+ _commandBuffer.GenerateMix(
+ mix.BufferOffset + bufferIndex,
+ destinationMix.BufferOffset + bufferDestinationIndex,
+ mix.NodeId,
+ volume);
}
}
}
@@ -783,11 +1008,12 @@ namespace Ryujinx.Audio.Renderer.Server
private void GenerateSubMix(ref MixState subMix)
{
- _commandBuffer.GenerateDepopForMixBuffersCommand(_rendererContext.DepopBuffer,
- subMix.BufferOffset,
- subMix.BufferCount,
- subMix.NodeId,
- subMix.SampleRate);
+ _commandBuffer.GenerateDepopForMixBuffers(
+ _rendererContext.DepopBuffer,
+ subMix.BufferOffset,
+ subMix.BufferCount,
+ subMix.NodeId,
+ subMix.SampleRate);
GenerateEffects(ref subMix);
@@ -847,11 +1073,12 @@ namespace Ryujinx.Audio.Renderer.Server
{
ref MixState finalMix = ref _mixContext.GetFinalState();
- _commandBuffer.GenerateDepopForMixBuffersCommand(_rendererContext.DepopBuffer,
- finalMix.BufferOffset,
- finalMix.BufferCount,
- finalMix.NodeId,
- finalMix.SampleRate);
+ _commandBuffer.GenerateDepopForMixBuffers(
+ _rendererContext.DepopBuffer,
+ finalMix.BufferOffset,
+ finalMix.BufferCount,
+ finalMix.NodeId,
+ finalMix.SampleRate);
GenerateEffects(ref finalMix);
@@ -882,9 +1109,10 @@ namespace Ryujinx.Audio.Renderer.Server
GeneratePerformance(ref performanceEntry, PerformanceCommand.Type.Start, nodeId);
}
- _commandBuffer.GenerateVolume(finalMix.Volume,
- finalMix.BufferOffset + bufferIndex,
- nodeId);
+ _commandBuffer.GenerateVolume(
+ finalMix.Volume,
+ finalMix.BufferOffset + bufferIndex,
+ nodeId);
if (performanceSubInitialized)
{
@@ -938,41 +1166,45 @@ namespace Ryujinx.Audio.Renderer.Server
if (useCustomDownMixingCommand)
{
- _commandBuffer.GenerateDownMixSurroundToStereo(finalMix.BufferOffset,
- sink.Parameter.Input.AsSpan(),
- sink.Parameter.Input.AsSpan(),
- sink.DownMixCoefficients,
- Constants.InvalidNodeId);
+ _commandBuffer.GenerateDownMixSurroundToStereo(
+ finalMix.BufferOffset,
+ sink.Parameter.Input.AsSpan(),
+ sink.Parameter.Input.AsSpan(),
+ sink.DownMixCoefficients,
+ Constants.InvalidNodeId);
}
// NOTE: We do the downmixing at the DSP level as it's easier that way.
else if (_rendererContext.ChannelCount == 2 && sink.Parameter.InputCount == 6)
{
- _commandBuffer.GenerateDownMixSurroundToStereo(finalMix.BufferOffset,
- sink.Parameter.Input.AsSpan(),
- sink.Parameter.Input.AsSpan(),
- Constants.DefaultSurroundToStereoCoefficients,
- Constants.InvalidNodeId);
+ _commandBuffer.GenerateDownMixSurroundToStereo(
+ finalMix.BufferOffset,
+ sink.Parameter.Input.AsSpan(),
+ sink.Parameter.Input.AsSpan(),
+ Constants.DefaultSurroundToStereoCoefficients,
+ Constants.InvalidNodeId);
}
CommandList commandList = _commandBuffer.CommandList;
if (sink.UpsamplerState != null)
{
- _commandBuffer.GenerateUpsample(finalMix.BufferOffset,
- sink.UpsamplerState,
- sink.Parameter.InputCount,
- sink.Parameter.Input.AsSpan(),
- commandList.BufferCount,
- commandList.SampleCount,
- commandList.SampleRate,
- Constants.InvalidNodeId);
+ _commandBuffer.GenerateUpsample(
+ finalMix.BufferOffset,
+ sink.UpsamplerState,
+ sink.Parameter.InputCount,
+ sink.Parameter.Input.AsSpan(),
+ commandList.BufferCount,
+ commandList.SampleCount,
+ commandList.SampleRate,
+ Constants.InvalidNodeId);
}
- _commandBuffer.GenerateDeviceSink(finalMix.BufferOffset,
- sink,
- _rendererContext.SessionId,
- commandList.Buffers,
- Constants.InvalidNodeId);
+ _commandBuffer.GenerateDeviceSink(
+ finalMix.BufferOffset,
+ sink,
+ _rendererContext.SessionId,
+ commandList.Buffers,
+ Constants.InvalidNodeId);
}
private void GenerateSink(BaseSink sink, ref MixState finalMix)
diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs
index d95e9aa71..cff754b82 100644
--- a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion1.cs
@@ -170,7 +170,7 @@ namespace Ryujinx.Audio.Renderer.Server
return 0;
}
- public uint Estimate(GroupedBiquadFilterCommand command)
+ public uint Estimate(MultiTapBiquadFilterCommand command)
{
return 0;
}
@@ -184,5 +184,15 @@ namespace Ryujinx.Audio.Renderer.Server
{
return 0;
}
+
+ public uint Estimate(BiquadFilterAndMixCommand command)
+ {
+ return 0;
+ }
+
+ public uint Estimate(MultiTapBiquadFilterAndMixCommand command)
+ {
+ return 0;
+ }
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs
index 929aaf383..ef1326924 100644
--- a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion2.cs
@@ -462,7 +462,7 @@ namespace Ryujinx.Audio.Renderer.Server
return 0;
}
- public uint Estimate(GroupedBiquadFilterCommand command)
+ public uint Estimate(MultiTapBiquadFilterCommand command)
{
return 0;
}
@@ -476,5 +476,15 @@ namespace Ryujinx.Audio.Renderer.Server
{
return 0;
}
+
+ public uint Estimate(BiquadFilterAndMixCommand command)
+ {
+ return 0;
+ }
+
+ public uint Estimate(MultiTapBiquadFilterAndMixCommand command)
+ {
+ return 0;
+ }
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs
index 8ae4bc059..31a5347b4 100644
--- a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion3.cs
@@ -632,7 +632,7 @@ namespace Ryujinx.Audio.Renderer.Server
};
}
- public virtual uint Estimate(GroupedBiquadFilterCommand command)
+ public virtual uint Estimate(MultiTapBiquadFilterCommand command)
{
return 0;
}
@@ -646,5 +646,15 @@ namespace Ryujinx.Audio.Renderer.Server
{
return 0;
}
+
+ public virtual uint Estimate(BiquadFilterAndMixCommand command)
+ {
+ return 0;
+ }
+
+ public virtual uint Estimate(MultiTapBiquadFilterAndMixCommand command)
+ {
+ return 0;
+ }
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion4.cs b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion4.cs
index 25bc67cd9..fb357120d 100644
--- a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion4.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion4.cs
@@ -10,7 +10,7 @@ namespace Ryujinx.Audio.Renderer.Server
{
public CommandProcessingTimeEstimatorVersion4(uint sampleCount, uint bufferCount) : base(sampleCount, bufferCount) { }
- public override uint Estimate(GroupedBiquadFilterCommand command)
+ public override uint Estimate(MultiTapBiquadFilterCommand command)
{
Debug.Assert(SampleCount == 160 || SampleCount == 240);
diff --git a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs
index 7135c1c4f..06f135a88 100644
--- a/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/CommandProcessingTimeEstimatorVersion5.cs
@@ -210,5 +210,53 @@ namespace Ryujinx.Audio.Renderer.Server
_ => throw new NotImplementedException($"{command.Parameter.ChannelCount}"),
};
}
+
+ public override uint Estimate(BiquadFilterAndMixCommand command)
+ {
+ Debug.Assert(SampleCount == 160 || SampleCount == 240);
+
+ if (command.HasVolumeRamp)
+ {
+ if (SampleCount == 160)
+ {
+ return 5204;
+ }
+
+ return 6683;
+ }
+ else
+ {
+ if (SampleCount == 160)
+ {
+ return 3427;
+ }
+
+ return 4752;
+ }
+ }
+
+ public override uint Estimate(MultiTapBiquadFilterAndMixCommand command)
+ {
+ Debug.Assert(SampleCount == 160 || SampleCount == 240);
+
+ if (command.HasVolumeRamp)
+ {
+ if (SampleCount == 160)
+ {
+ return 7939;
+ }
+
+ return 10669;
+ }
+ else
+ {
+ if (SampleCount == 160)
+ {
+ return 6256;
+ }
+
+ return 8683;
+ }
+ }
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs b/src/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs
index 27b22363a..9c4312ad6 100644
--- a/src/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/ICommandProcessingTimeEstimator.cs
@@ -33,8 +33,10 @@ namespace Ryujinx.Audio.Renderer.Server
uint Estimate(UpsampleCommand command);
uint Estimate(LimiterCommandVersion1 command);
uint Estimate(LimiterCommandVersion2 command);
- uint Estimate(GroupedBiquadFilterCommand command);
+ uint Estimate(MultiTapBiquadFilterCommand command);
uint Estimate(CaptureBufferCommand command);
uint Estimate(CompressorCommand command);
+ uint Estimate(BiquadFilterAndMixCommand command);
+ uint Estimate(MultiTapBiquadFilterAndMixCommand command);
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs b/src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs
index b90574da9..5ba58ea5b 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Mix/MixState.cs
@@ -225,11 +225,11 @@ namespace Ryujinx.Audio.Renderer.Server.Mix
for (int i = 0; i < splitter.DestinationCount; i++)
{
- Span destination = splitter.GetData(i);
+ SplitterDestination destination = splitter.GetData(i);
- if (!destination.IsEmpty)
+ if (!destination.IsNull)
{
- int destinationMixId = destination[0].DestinationId;
+ int destinationMixId = destination.DestinationId;
if (destinationMixId != UnusedMixId)
{
diff --git a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs
index 3efa783c3..a7b82a6bd 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterContext.cs
@@ -1,4 +1,5 @@
using Ryujinx.Audio.Renderer.Common;
+using Ryujinx.Audio.Renderer.Dsp.State;
using Ryujinx.Audio.Renderer.Parameter;
using Ryujinx.Audio.Renderer.Utils;
using Ryujinx.Common;
@@ -15,15 +16,35 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
///
public class SplitterContext
{
+ ///
+ /// Amount of biquad filter states per splitter destination.
+ ///
+ public const int BqfStatesPerDestination = 4;
+
///
/// Storage for .
///
private Memory _splitters;
///
- /// Storage for .
+ /// Storage for .
///
- private Memory _splitterDestinations;
+ private Memory _splitterDestinationsV1;
+
+ ///
+ /// Storage for .
+ ///
+ private Memory _splitterDestinationsV2;
+
+ ///
+ /// Splitter biquad filtering states.
+ ///
+ private Memory _splitterBqfStates;
+
+ ///
+ /// Version of the splitter context that is being used, currently can be 1 or 2.
+ ///
+ public int Version { get; private set; }
///
/// If set to true, trust the user destination count in .
@@ -36,12 +57,17 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// The behaviour context.
/// The audio renderer configuration.
/// The .
+ /// Memory to store the biquad filtering state for splitters during processing.
/// Return true if the initialization was successful.
- public bool Initialize(ref BehaviourContext behaviourContext, ref AudioRendererConfiguration parameter, WorkBufferAllocator workBufferAllocator)
+ public bool Initialize(
+ ref BehaviourContext behaviourContext,
+ ref AudioRendererConfiguration parameter,
+ WorkBufferAllocator workBufferAllocator,
+ Memory splitterBqfStates)
{
if (!behaviourContext.IsSplitterSupported() || parameter.SplitterCount <= 0 || parameter.SplitterDestinationCount <= 0)
{
- Setup(Memory.Empty, Memory.Empty, false);
+ Setup(Memory.Empty, Memory.Empty, Memory.Empty, false);
return true;
}
@@ -60,23 +86,62 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
splitter = new SplitterState(splitterId++);
}
- Memory splitterDestinations = workBufferAllocator.Allocate(parameter.SplitterDestinationCount,
- SplitterDestination.Alignment);
+ Memory splitterDestinationsV1 = Memory.Empty;
+ Memory splitterDestinationsV2 = Memory.Empty;
- if (splitterDestinations.IsEmpty)
+ if (!behaviourContext.IsBiquadFilterParameterForSplitterEnabled())
{
- return false;
+ Version = 1;
+
+ splitterDestinationsV1 = workBufferAllocator.Allocate(parameter.SplitterDestinationCount,
+ SplitterDestinationVersion1.Alignment);
+
+ if (splitterDestinationsV1.IsEmpty)
+ {
+ return false;
+ }
+
+ int splitterDestinationId = 0;
+ foreach (ref SplitterDestinationVersion1 data in splitterDestinationsV1.Span)
+ {
+ data = new SplitterDestinationVersion1(splitterDestinationId++);
+ }
}
-
- int splitterDestinationId = 0;
- foreach (ref SplitterDestination data in splitterDestinations.Span)
+ else
{
- data = new SplitterDestination(splitterDestinationId++);
+ Version = 2;
+
+ splitterDestinationsV2 = workBufferAllocator.Allocate(parameter.SplitterDestinationCount,
+ SplitterDestinationVersion2.Alignment);
+
+ if (splitterDestinationsV2.IsEmpty)
+ {
+ return false;
+ }
+
+ int splitterDestinationId = 0;
+ foreach (ref SplitterDestinationVersion2 data in splitterDestinationsV2.Span)
+ {
+ data = new SplitterDestinationVersion2(splitterDestinationId++);
+ }
+
+ if (parameter.SplitterDestinationCount > 0)
+ {
+ // Official code stores it in the SplitterDestinationVersion2 struct,
+ // but we don't to avoid using unsafe code.
+
+ splitterBqfStates.Span.Clear();
+ _splitterBqfStates = splitterBqfStates;
+ }
+ else
+ {
+ _splitterBqfStates = Memory.Empty;
+ }
}
SplitterState.InitializeSplitters(splitters.Span);
- Setup(splitters, splitterDestinations, behaviourContext.IsSplitterBugFixed());
+ Setup(splitters, splitterDestinationsV1, splitterDestinationsV2, behaviourContext.IsSplitterBugFixed());
return true;
}
@@ -93,7 +158,15 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
if (behaviourContext.IsSplitterSupported())
{
size = WorkBufferAllocator.GetTargetSize(size, parameter.SplitterCount, SplitterState.Alignment);
- size = WorkBufferAllocator.GetTargetSize(size, parameter.SplitterDestinationCount, SplitterDestination.Alignment);
+
+ if (behaviourContext.IsBiquadFilterParameterForSplitterEnabled())
+ {
+ size = WorkBufferAllocator.GetTargetSize(size, parameter.SplitterDestinationCount, SplitterDestinationVersion2.Alignment);
+ }
+ else
+ {
+ size = WorkBufferAllocator.GetTargetSize(size, parameter.SplitterDestinationCount, SplitterDestinationVersion1.Alignment);
+ }
if (behaviourContext.IsSplitterBugFixed())
{
@@ -110,12 +183,18 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// Setup the instance.
///
/// The storage.
- /// The storage.
+ /// The storage.
+ /// The storage.
/// If set to true, trust the user destination count in .
- private void Setup(Memory splitters, Memory splitterDestinations, bool isBugFixed)
+ private void Setup(
+ Memory splitters,
+ Memory splitterDestinationsV1,
+ Memory splitterDestinationsV2,
+ bool isBugFixed)
{
_splitters = splitters;
- _splitterDestinations = splitterDestinations;
+ _splitterDestinationsV1 = splitterDestinationsV1;
+ _splitterDestinationsV2 = splitterDestinationsV2;
IsBugFixed = isBugFixed;
}
@@ -141,7 +220,9 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
return 0;
}
- return _splitterDestinations.Length / _splitters.Length;
+ int length = _splitterDestinationsV2.IsEmpty ? _splitterDestinationsV1.Length : _splitterDestinationsV2.Length;
+
+ return length / _splitters.Length;
}
///
@@ -178,7 +259,39 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
}
///
- /// Update one or multiple from user parameters.
+ /// Update one splitter destination data from user parameters.
+ ///
+ /// The raw data after the splitter header.
+ /// True if the update was successful, false otherwise
+ private bool UpdateData(ref SequenceReader input) where T : unmanaged, ISplitterDestinationInParameter
+ {
+ ref readonly T parameter = ref input.GetRefOrRefToCopy(out _);
+
+ Debug.Assert(parameter.IsMagicValid());
+
+ if (parameter.IsMagicValid())
+ {
+ int length = _splitterDestinationsV2.IsEmpty ? _splitterDestinationsV1.Length : _splitterDestinationsV2.Length;
+
+ if (parameter.Id >= 0 && parameter.Id < length)
+ {
+ SplitterDestination destination = GetDestination(parameter.Id);
+
+ destination.Update(parameter);
+ }
+
+ return true;
+ }
+ else
+ {
+ input.Rewind(Unsafe.SizeOf());
+
+ return false;
+ }
+ }
+
+ ///
+ /// Update one or multiple splitter destination data from user parameters.
///
/// The splitter header.
/// The raw data after the splitter header.
@@ -186,23 +299,23 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
{
for (int i = 0; i < inputHeader.SplitterDestinationCount; i++)
{
- ref readonly SplitterDestinationInParameter parameter = ref input.GetRefOrRefToCopy(out _);
-
- Debug.Assert(parameter.IsMagicValid());
-
- if (parameter.IsMagicValid())
+ if (Version == 1)
{
- if (parameter.Id >= 0 && parameter.Id < _splitterDestinations.Length)
+ if (!UpdateData(ref input))
{
- ref SplitterDestination destination = ref GetDestination(parameter.Id);
-
- destination.Update(parameter);
+ break;
+ }
+ }
+ else if (Version == 2)
+ {
+ if (!UpdateData(ref input))
+ {
+ break;
}
}
else
{
- input.Rewind(Unsafe.SizeOf());
- break;
+ Debug.Fail($"Invalid splitter context version {Version}.");
}
}
}
@@ -214,7 +327,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// Return true if the update was successful.
public bool Update(ref SequenceReader input)
{
- if (_splitterDestinations.IsEmpty || _splitters.IsEmpty)
+ if (!UsingSplitter())
{
return true;
}
@@ -251,45 +364,52 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
}
///
- /// Get a reference to a at the given .
+ /// Get a reference to the splitter destination data at the given .
///
/// The index to use.
- /// A reference to a at the given .
- public ref SplitterDestination GetDestination(int id)
+ /// A reference to the splitter destination data at the given .
+ public SplitterDestination GetDestination(int id)
{
- return ref SpanIOHelper.GetFromMemory(_splitterDestinations, id, (uint)_splitterDestinations.Length);
+ if (_splitterDestinationsV2.IsEmpty)
+ {
+ return new SplitterDestination(ref SpanIOHelper.GetFromMemory(_splitterDestinationsV1, id, (uint)_splitterDestinationsV1.Length));
+ }
+ else
+ {
+ return new SplitterDestination(ref SpanIOHelper.GetFromMemory(_splitterDestinationsV2, id, (uint)_splitterDestinationsV2.Length));
+ }
}
///
- /// Get a at the given .
- ///
- /// The index to use.
- /// A at the given .
- public Memory GetDestinationMemory(int id)
- {
- return SpanIOHelper.GetMemory(_splitterDestinations, id, (uint)_splitterDestinations.Length);
- }
-
- ///
- /// Get a in the at and pass to .
+ /// Get a in the at and pass to .
///
/// The index to use to get the .
/// The index of the .
- /// A .
- public Span GetDestination(int id, int destinationId)
+ /// A .
+ public SplitterDestination GetDestination(int id, int destinationId)
{
ref SplitterState splitter = ref GetState(id);
return splitter.GetData(destinationId);
}
+ ///
+ /// Gets the biquad filter state for a given splitter destination.
+ ///
+ /// The splitter destination.
+ /// Biquad filter state for the specified destination.
+ public Memory GetBiquadFilterState(SplitterDestination destination)
+ {
+ return _splitterBqfStates.Slice(destination.Id * BqfStatesPerDestination, BqfStatesPerDestination);
+ }
+
///
/// Return true if the audio renderer has any splitters.
///
/// True if the audio renderer has any splitters.
public bool UsingSplitter()
{
- return !_splitters.IsEmpty && !_splitterDestinations.IsEmpty;
+ return !_splitters.IsEmpty && (!_splitterDestinationsV1.IsEmpty || !_splitterDestinationsV2.IsEmpty);
}
///
diff --git a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs
index 1faf7921f..36dfa5e41 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestination.cs
@@ -1,115 +1,198 @@
using Ryujinx.Audio.Renderer.Parameter;
-using Ryujinx.Common.Utilities;
using System;
using System.Diagnostics;
-using System.Runtime.InteropServices;
+using System.Runtime.CompilerServices;
namespace Ryujinx.Audio.Renderer.Server.Splitter
{
///
/// Server state for a splitter destination.
///
- [StructLayout(LayoutKind.Sequential, Size = 0xE0, Pack = Alignment)]
- public struct SplitterDestination
+ public ref struct SplitterDestination
{
- public const int Alignment = 0x10;
+ private ref SplitterDestinationVersion1 _v1;
+ private ref SplitterDestinationVersion2 _v2;
///
- /// The unique id of this .
+ /// Checks if the splitter destination data reference is null.
///
- public int Id;
+ public bool IsNull => Unsafe.IsNullRef(ref _v1) && Unsafe.IsNullRef(ref _v2);
///
- /// The mix to output the result of the splitter.
+ /// The splitter unique id.
///
- public int DestinationId;
-
- ///
- /// Mix buffer volumes storage.
- ///
- private MixArray _mix;
- private MixArray _previousMix;
-
- ///
- /// Pointer to the next linked element.
- ///
- private unsafe SplitterDestination* _next;
-
- ///
- /// Set to true if in use.
- ///
- [MarshalAs(UnmanagedType.I1)]
- public bool IsUsed;
-
- ///
- /// Set to true if the internal state need to be updated.
- ///
- [MarshalAs(UnmanagedType.I1)]
- public bool NeedToUpdateInternalState;
-
- [StructLayout(LayoutKind.Sequential, Size = 4 * Constants.MixBufferCountMax, Pack = 1)]
- private struct MixArray { }
-
- ///
- /// Mix buffer volumes.
- ///
- /// Used when a splitter id is specified in the mix.
- public Span MixBufferVolume => SpanHelpers.AsSpan(ref _mix);
-
- ///
- /// Previous mix buffer volumes.
- ///
- /// Used when a splitter id is specified in the mix.
- public Span PreviousMixBufferVolume => SpanHelpers.AsSpan(ref _previousMix);
-
- ///
- /// Get the of the next element or if not present.
- ///
- public readonly Span Next
+ public int Id
{
get
{
- unsafe
+ if (Unsafe.IsNullRef(ref _v2))
{
- return _next != null ? new Span(_next, 1) : Span.Empty;
+ if (Unsafe.IsNullRef(ref _v1))
+ {
+ return 0;
+ }
+ else
+ {
+ return _v1.Id;
+ }
+ }
+ else
+ {
+ return _v2.Id;
}
}
}
///
- /// Create a new .
+ /// The mix to output the result of the splitter.
///
- /// The unique id of this .
- public SplitterDestination(int id) : this()
+ public int DestinationId
{
- Id = id;
- DestinationId = Constants.UnusedMixId;
-
- ClearVolumes();
+ get
+ {
+ if (Unsafe.IsNullRef(ref _v2))
+ {
+ if (Unsafe.IsNullRef(ref _v1))
+ {
+ return 0;
+ }
+ else
+ {
+ return _v1.DestinationId;
+ }
+ }
+ else
+ {
+ return _v2.DestinationId;
+ }
+ }
}
///
- /// Update the from user parameter.
+ /// Mix buffer volumes.
+ ///
+ /// Used when a splitter id is specified in the mix.
+ public Span MixBufferVolume
+ {
+ get
+ {
+ if (Unsafe.IsNullRef(ref _v2))
+ {
+ if (Unsafe.IsNullRef(ref _v1))
+ {
+ return Span.Empty;
+ }
+ else
+ {
+ return _v1.MixBufferVolume;
+ }
+ }
+ else
+ {
+ return _v2.MixBufferVolume;
+ }
+ }
+ }
+
+ ///
+ /// Previous mix buffer volumes.
+ ///
+ /// Used when a splitter id is specified in the mix.
+ public Span PreviousMixBufferVolume
+ {
+ get
+ {
+ if (Unsafe.IsNullRef(ref _v2))
+ {
+ if (Unsafe.IsNullRef(ref _v1))
+ {
+ return Span.Empty;
+ }
+ else
+ {
+ return _v1.PreviousMixBufferVolume;
+ }
+ }
+ else
+ {
+ return _v2.PreviousMixBufferVolume;
+ }
+ }
+ }
+
+ ///
+ /// Get the of the next element or null if not present.
+ ///
+ public readonly SplitterDestination Next
+ {
+ get
+ {
+ unsafe
+ {
+ if (Unsafe.IsNullRef(ref _v2))
+ {
+ if (Unsafe.IsNullRef(ref _v1))
+ {
+ return new SplitterDestination();
+ }
+ else
+ {
+ return new SplitterDestination(ref _v1.Next);
+ }
+ }
+ else
+ {
+ return new SplitterDestination(ref _v2.Next);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Creates a new splitter destination wrapper for the version 1 splitter destination data.
+ ///
+ /// Version 1 splitter destination data
+ public SplitterDestination(ref SplitterDestinationVersion1 v1)
+ {
+ _v1 = ref v1;
+ _v2 = ref Unsafe.NullRef();
+ }
+
+ ///
+ /// Creates a new splitter destination wrapper for the version 2 splitter destination data.
+ ///
+ /// Version 2 splitter destination data
+ public SplitterDestination(ref SplitterDestinationVersion2 v2)
+ {
+
+ _v1 = ref Unsafe.NullRef();
+ _v2 = ref v2;
+ }
+
+ ///
+ /// Creates a new splitter destination wrapper for the splitter destination data.
+ ///
+ /// Version 1 splitter destination data
+ /// Version 2 splitter destination data
+ public unsafe SplitterDestination(SplitterDestinationVersion1* v1, SplitterDestinationVersion2* v2)
+ {
+ _v1 = ref Unsafe.AsRef(v1);
+ _v2 = ref Unsafe.AsRef(v2);
+ }
+
+ ///
+ /// Update the splitter destination data from user parameter.
///
/// The user parameter.
- public void Update(SplitterDestinationInParameter parameter)
+ public void Update(in T parameter) where T : ISplitterDestinationInParameter
{
- Debug.Assert(Id == parameter.Id);
-
- if (parameter.IsMagicValid() && Id == parameter.Id)
+ if (Unsafe.IsNullRef(ref _v2))
{
- DestinationId = parameter.DestinationId;
-
- parameter.MixBufferVolume.CopyTo(MixBufferVolume);
-
- if (!IsUsed && parameter.IsUsed)
- {
- MixBufferVolume.CopyTo(PreviousMixBufferVolume);
-
- NeedToUpdateInternalState = false;
- }
-
- IsUsed = parameter.IsUsed;
+ _v1.Update(parameter);
+ }
+ else
+ {
+ _v2.Update(parameter);
}
}
@@ -118,12 +201,14 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
///
public void UpdateInternalState()
{
- if (IsUsed && NeedToUpdateInternalState)
+ if (Unsafe.IsNullRef(ref _v2))
{
- MixBufferVolume.CopyTo(PreviousMixBufferVolume);
+ _v1.UpdateInternalState();
+ }
+ else
+ {
+ _v2.UpdateInternalState();
}
-
- NeedToUpdateInternalState = false;
}
///
@@ -131,16 +216,23 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
///
public void MarkAsNeedToUpdateInternalState()
{
- NeedToUpdateInternalState = true;
+ if (Unsafe.IsNullRef(ref _v2))
+ {
+ _v1.MarkAsNeedToUpdateInternalState();
+ }
+ else
+ {
+ _v2.MarkAsNeedToUpdateInternalState();
+ }
}
///
- /// Return true if the is used and has a destination.
+ /// Return true if the splitter destination is used and has a destination.
///
- /// True if the is used and has a destination.
+ /// True if the splitter destination is used and has a destination.
public readonly bool IsConfigured()
{
- return IsUsed && DestinationId != Constants.UnusedMixId;
+ return Unsafe.IsNullRef(ref _v2) ? _v1.IsConfigured() : _v2.IsConfigured();
}
///
@@ -150,9 +242,17 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
/// The volume for the given destination.
public float GetMixVolume(int destinationIndex)
{
- Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
+ return Unsafe.IsNullRef(ref _v2) ? _v1.GetMixVolume(destinationIndex) : _v2.GetMixVolume(destinationIndex);
+ }
- return MixBufferVolume[destinationIndex];
+ ///
+ /// Get the previous volume for a given destination.
+ ///
+ /// The destination index to use.
+ /// The volume for the given destination.
+ public float GetMixVolumePrev(int destinationIndex)
+ {
+ return Unsafe.IsNullRef(ref _v2) ? _v1.GetMixVolumePrev(destinationIndex) : _v2.GetMixVolumePrev(destinationIndex);
}
///
@@ -160,22 +260,33 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
///
public void ClearVolumes()
{
- MixBufferVolume.Clear();
- PreviousMixBufferVolume.Clear();
+ if (Unsafe.IsNullRef(ref _v2))
+ {
+ _v1.ClearVolumes();
+ }
+ else
+ {
+ _v2.ClearVolumes();
+ }
}
///
- /// Link the next element to the given .
+ /// Link the next element to the given splitter destination.
///
- /// The given to link.
- public void Link(ref SplitterDestination next)
+ /// The given splitter destination to link.
+ public void Link(SplitterDestination next)
{
- unsafe
+ if (Unsafe.IsNullRef(ref _v2))
{
- fixed (SplitterDestination* nextPtr = &next)
- {
- _next = nextPtr;
- }
+ Debug.Assert(!Unsafe.IsNullRef(ref next._v1));
+
+ _v1.Link(ref next._v1);
+ }
+ else
+ {
+ Debug.Assert(!Unsafe.IsNullRef(ref next._v2));
+
+ _v2.Link(ref next._v2);
}
}
@@ -184,10 +295,74 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
///
public void Unlink()
{
- unsafe
+ if (Unsafe.IsNullRef(ref _v2))
{
- _next = null;
+ _v1.Unlink();
}
+ else
+ {
+ _v2.Unlink();
+ }
+ }
+
+ ///
+ /// Checks if any biquad filter is enabled.
+ ///
+ /// True if any biquad filter is enabled.
+ public bool IsBiquadFilterEnabled()
+ {
+ return !Unsafe.IsNullRef(ref _v2) && _v2.IsBiquadFilterEnabled();
+ }
+
+ ///
+ /// Checks if any biquad filter was previously enabled.
+ ///
+ /// True if any biquad filter was previously enabled.
+ public bool IsBiquadFilterEnabledPrev()
+ {
+ return !Unsafe.IsNullRef(ref _v2) && _v2.IsBiquadFilterEnabledPrev();
+ }
+
+ ///
+ /// Gets the biquad filter parameters.
+ ///
+ /// Biquad filter index (0 or 1).
+ /// Biquad filter parameters.
+ public ref BiquadFilterParameter GetBiquadFilterParameter(int index)
+ {
+ Debug.Assert(!Unsafe.IsNullRef(ref _v2));
+
+ return ref _v2.GetBiquadFilterParameter(index);
+ }
+
+ ///
+ /// Checks if any biquad filter was previously enabled.
+ ///
+ /// Biquad filter index (0 or 1).
+ public void UpdateBiquadFilterEnabledPrev(int index)
+ {
+ if (!Unsafe.IsNullRef(ref _v2))
+ {
+ _v2.UpdateBiquadFilterEnabledPrev(index);
+ }
+ }
+
+ ///
+ /// Get the reference for the version 1 splitter destination data, or null if version 2 is being used or the destination is null.
+ ///
+ /// Reference for the version 1 splitter destination data.
+ public ref SplitterDestinationVersion1 GetV1RefOrNull()
+ {
+ return ref _v1;
+ }
+
+ ///
+ /// Get the reference for the version 2 splitter destination data, or null if version 1 is being used or the destination is null.
+ ///
+ /// Reference for the version 2 splitter destination data.
+ public ref SplitterDestinationVersion2 GetV2RefOrNull()
+ {
+ return ref _v2;
}
}
}
diff --git a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion1.cs b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion1.cs
new file mode 100644
index 000000000..5d2b8fb0f
--- /dev/null
+++ b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion1.cs
@@ -0,0 +1,206 @@
+using Ryujinx.Audio.Renderer.Parameter;
+using Ryujinx.Common.Utilities;
+using System;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Audio.Renderer.Server.Splitter
+{
+ ///
+ /// Server state for a splitter destination (version 1).
+ ///
+ [StructLayout(LayoutKind.Sequential, Size = 0xE0, Pack = Alignment)]
+ public struct SplitterDestinationVersion1
+ {
+ public const int Alignment = 0x10;
+
+ ///
+ /// The unique id of this .
+ ///
+ public int Id;
+
+ ///
+ /// The mix to output the result of the splitter.
+ ///
+ public int DestinationId;
+
+ ///
+ /// Mix buffer volumes storage.
+ ///
+ private MixArray _mix;
+ private MixArray _previousMix;
+
+ ///
+ /// Pointer to the next linked element.
+ ///
+ private unsafe SplitterDestinationVersion1* _next;
+
+ ///
+ /// Set to true if in use.
+ ///
+ [MarshalAs(UnmanagedType.I1)]
+ public bool IsUsed;
+
+ ///
+ /// Set to true if the internal state need to be updated.
+ ///
+ [MarshalAs(UnmanagedType.I1)]
+ public bool NeedToUpdateInternalState;
+
+ [StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
+ private struct MixArray { }
+
+ ///
+ /// Mix buffer volumes.
+ ///
+ /// Used when a splitter id is specified in the mix.
+ public Span MixBufferVolume => SpanHelpers.AsSpan(ref _mix);
+
+ ///
+ /// Previous mix buffer volumes.
+ ///
+ /// Used when a splitter id is specified in the mix.
+ public Span PreviousMixBufferVolume => SpanHelpers.AsSpan(ref _previousMix);
+
+ ///
+ /// Get the reference of the next element or null if not present.
+ ///
+ public readonly ref SplitterDestinationVersion1 Next
+ {
+ get
+ {
+ unsafe
+ {
+ return ref Unsafe.AsRef(_next);
+ }
+ }
+ }
+
+ ///
+ /// Create a new .
+ ///
+ /// The unique id of this .
+ public SplitterDestinationVersion1(int id) : this()
+ {
+ Id = id;
+ DestinationId = Constants.UnusedMixId;
+
+ ClearVolumes();
+ }
+
+ ///
+ /// Update the from user parameter.
+ ///
+ /// The user parameter.
+ public void Update(in T parameter) where T : ISplitterDestinationInParameter
+ {
+ Debug.Assert(Id == parameter.Id);
+
+ if (parameter.IsMagicValid() && Id == parameter.Id)
+ {
+ DestinationId = parameter.DestinationId;
+
+ parameter.MixBufferVolume.CopyTo(MixBufferVolume);
+
+ if (!IsUsed && parameter.IsUsed)
+ {
+ MixBufferVolume.CopyTo(PreviousMixBufferVolume);
+
+ NeedToUpdateInternalState = false;
+ }
+
+ IsUsed = parameter.IsUsed;
+ }
+ }
+
+ ///
+ /// Update the internal state of the instance.
+ ///
+ public void UpdateInternalState()
+ {
+ if (IsUsed && NeedToUpdateInternalState)
+ {
+ MixBufferVolume.CopyTo(PreviousMixBufferVolume);
+ }
+
+ NeedToUpdateInternalState = false;
+ }
+
+ ///
+ /// Set the update internal state marker.
+ ///
+ public void MarkAsNeedToUpdateInternalState()
+ {
+ NeedToUpdateInternalState = true;
+ }
+
+ ///
+ /// Return true if the is used and has a destination.
+ ///
+ /// True if the is used and has a destination.
+ public readonly bool IsConfigured()
+ {
+ return IsUsed && DestinationId != Constants.UnusedMixId;
+ }
+
+ ///
+ /// Get the volume for a given destination.
+ ///
+ /// The destination index to use.
+ /// The volume for the given destination.
+ public float GetMixVolume(int destinationIndex)
+ {
+ Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
+
+ return MixBufferVolume[destinationIndex];
+ }
+
+ ///
+ /// Get the previous volume for a given destination.
+ ///
+ /// The destination index to use.
+ /// The volume for the given destination.
+ public float GetMixVolumePrev(int destinationIndex)
+ {
+ Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
+
+ return PreviousMixBufferVolume[destinationIndex];
+ }
+
+ ///
+ /// Clear the volumes.
+ ///
+ public void ClearVolumes()
+ {
+ MixBufferVolume.Clear();
+ PreviousMixBufferVolume.Clear();
+ }
+
+ ///
+ /// Link the next element to the given .
+ ///
+ /// The given to link.
+ public void Link(ref SplitterDestinationVersion1 next)
+ {
+ unsafe
+ {
+ fixed (SplitterDestinationVersion1* nextPtr = &next)
+ {
+ _next = nextPtr;
+ }
+ }
+ }
+
+ ///
+ /// Remove the link to the next element.
+ ///
+ public void Unlink()
+ {
+ unsafe
+ {
+ _next = null;
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion2.cs b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion2.cs
new file mode 100644
index 000000000..f9487909d
--- /dev/null
+++ b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterDestinationVersion2.cs
@@ -0,0 +1,250 @@
+using Ryujinx.Audio.Renderer.Parameter;
+using Ryujinx.Common.Memory;
+using Ryujinx.Common.Utilities;
+using System;
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Audio.Renderer.Server.Splitter
+{
+ ///
+ /// Server state for a splitter destination (version 2).
+ ///
+ [StructLayout(LayoutKind.Sequential, Size = 0x110, Pack = Alignment)]
+ public struct SplitterDestinationVersion2
+ {
+ public const int Alignment = 0x10;
+
+ ///
+ /// The unique id of this .
+ ///
+ public int Id;
+
+ ///
+ /// The mix to output the result of the splitter.
+ ///
+ public int DestinationId;
+
+ ///
+ /// Mix buffer volumes storage.
+ ///
+ private MixArray _mix;
+ private MixArray _previousMix;
+
+ ///
+ /// Pointer to the next linked element.
+ ///
+ private unsafe SplitterDestinationVersion2* _next;
+
+ ///
+ /// Set to true if in use.
+ ///
+ [MarshalAs(UnmanagedType.I1)]
+ public bool IsUsed;
+
+ ///
+ /// Set to true if the internal state need to be updated.
+ ///
+ [MarshalAs(UnmanagedType.I1)]
+ public bool NeedToUpdateInternalState;
+
+ [StructLayout(LayoutKind.Sequential, Size = sizeof(float) * Constants.MixBufferCountMax, Pack = 1)]
+ private struct MixArray { }
+
+ ///
+ /// Mix buffer volumes.
+ ///
+ /// Used when a splitter id is specified in the mix.
+ public Span MixBufferVolume => SpanHelpers.AsSpan(ref _mix);
+
+ ///
+ /// Previous mix buffer volumes.
+ ///
+ /// Used when a splitter id is specified in the mix.
+ public Span PreviousMixBufferVolume => SpanHelpers.AsSpan(ref _previousMix);
+
+ ///
+ /// Get the reference of the next element or null if not present.
+ ///
+ public readonly ref SplitterDestinationVersion2 Next
+ {
+ get
+ {
+ unsafe
+ {
+ return ref Unsafe.AsRef(_next);
+ }
+ }
+ }
+
+ private Array2 _biquadFilters;
+
+ private Array2 _isPreviousBiquadFilterEnabled;
+
+ ///
+ /// Create a new .
+ ///
+ /// The unique id of this .
+ public SplitterDestinationVersion2(int id) : this()
+ {
+ Id = id;
+ DestinationId = Constants.UnusedMixId;
+
+ ClearVolumes();
+ }
+
+ ///
+ /// Update the from user parameter.
+ ///
+ /// The user parameter.
+ public void Update(in T parameter) where T : ISplitterDestinationInParameter
+ {
+ Debug.Assert(Id == parameter.Id);
+
+ if (parameter.IsMagicValid() && Id == parameter.Id)
+ {
+ DestinationId = parameter.DestinationId;
+
+ parameter.MixBufferVolume.CopyTo(MixBufferVolume);
+
+ _biquadFilters = parameter.BiquadFilters;
+
+ if (!IsUsed && parameter.IsUsed)
+ {
+ MixBufferVolume.CopyTo(PreviousMixBufferVolume);
+
+ NeedToUpdateInternalState = false;
+ }
+
+ IsUsed = parameter.IsUsed;
+ }
+ }
+
+ ///
+ /// Update the internal state of the instance.
+ ///
+ public void UpdateInternalState()
+ {
+ if (IsUsed && NeedToUpdateInternalState)
+ {
+ MixBufferVolume.CopyTo(PreviousMixBufferVolume);
+ }
+
+ NeedToUpdateInternalState = false;
+ }
+
+ ///
+ /// Set the update internal state marker.
+ ///
+ public void MarkAsNeedToUpdateInternalState()
+ {
+ NeedToUpdateInternalState = true;
+ }
+
+ ///
+ /// Return true if the is used and has a destination.
+ ///
+ /// True if the is used and has a destination.
+ public readonly bool IsConfigured()
+ {
+ return IsUsed && DestinationId != Constants.UnusedMixId;
+ }
+
+ ///
+ /// Get the volume for a given destination.
+ ///
+ /// The destination index to use.
+ /// The volume for the given destination.
+ public float GetMixVolume(int destinationIndex)
+ {
+ Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
+
+ return MixBufferVolume[destinationIndex];
+ }
+
+ ///
+ /// Get the previous volume for a given destination.
+ ///
+ /// The destination index to use.
+ /// The volume for the given destination.
+ public float GetMixVolumePrev(int destinationIndex)
+ {
+ Debug.Assert(destinationIndex >= 0 && destinationIndex < Constants.MixBufferCountMax);
+
+ return PreviousMixBufferVolume[destinationIndex];
+ }
+
+ ///
+ /// Clear the volumes.
+ ///
+ public void ClearVolumes()
+ {
+ MixBufferVolume.Clear();
+ PreviousMixBufferVolume.Clear();
+ }
+
+ ///
+ /// Link the next element to the given .
+ ///
+ /// The given to link.
+ public void Link(ref SplitterDestinationVersion2 next)
+ {
+ unsafe
+ {
+ fixed (SplitterDestinationVersion2* nextPtr = &next)
+ {
+ _next = nextPtr;
+ }
+ }
+ }
+
+ ///
+ /// Remove the link to the next element.
+ ///
+ public void Unlink()
+ {
+ unsafe
+ {
+ _next = null;
+ }
+ }
+
+ ///
+ /// Checks if any biquad filter is enabled.
+ ///
+ /// True if any biquad filter is enabled.
+ public bool IsBiquadFilterEnabled()
+ {
+ return _biquadFilters[0].Enable || _biquadFilters[1].Enable;
+ }
+
+ ///
+ /// Checks if any biquad filter was previously enabled.
+ ///
+ /// True if any biquad filter was previously enabled.
+ public bool IsBiquadFilterEnabledPrev()
+ {
+ return _isPreviousBiquadFilterEnabled[0];
+ }
+
+ ///
+ /// Gets the biquad filter parameters.
+ ///
+ /// Biquad filter index (0 or 1).
+ /// Biquad filter parameters.
+ public ref BiquadFilterParameter GetBiquadFilterParameter(int index)
+ {
+ return ref _biquadFilters[index];
+ }
+
+ ///
+ /// Checks if any biquad filter was previously enabled.
+ ///
+ /// Biquad filter index (0 or 1).
+ public void UpdateBiquadFilterEnabledPrev(int index)
+ {
+ _isPreviousBiquadFilterEnabled[index] = _biquadFilters[index].Enable;
+ }
+ }
+}
diff --git a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs
index 944f092d2..3e7dce559 100644
--- a/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs
+++ b/src/Ryujinx.Audio/Renderer/Server/Splitter/SplitterState.cs
@@ -15,6 +15,8 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
{
public const int Alignment = 0x10;
+ private delegate void SplitterDestinationAction(SplitterDestination destination, int index);
+
///
/// The unique id of this .
///
@@ -26,7 +28,7 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
public uint SampleRate;
///
- /// Count of splitter destinations ().
+ /// Count of splitter destinations.
///
public int DestinationCount;
@@ -37,20 +39,25 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
public bool HasNewConnection;
///
- /// Linked list of .
+ /// Linked list of .
///
- private unsafe SplitterDestination* _destinationsData;
+ private unsafe SplitterDestinationVersion1* _destinationDataV1;
///
- /// Span to the first element of the linked list of .
+ /// Linked list of .
///
- public readonly Span Destinations
+ private unsafe SplitterDestinationVersion2* _destinationDataV2;
+
+ ///
+ /// First element of the linked list of splitter destinations data.
+ ///
+ public readonly SplitterDestination Destination
{
get
{
unsafe
{
- return (IntPtr)_destinationsData != IntPtr.Zero ? new Span(_destinationsData, 1) : Span.Empty;
+ return new SplitterDestination(_destinationDataV1, _destinationDataV2);
}
}
}
@@ -64,20 +71,20 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
Id = id;
}
- public readonly Span GetData(int index)
+ public readonly SplitterDestination GetData(int index)
{
int i = 0;
- Span result = Destinations;
+ SplitterDestination result = Destination;
while (i < index)
{
- if (result.IsEmpty)
+ if (result.IsNull)
{
break;
}
- result = result[0].Next;
+ result = result.Next;
i++;
}
@@ -93,25 +100,25 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
}
///
- /// Utility function to apply a given to all .
+ /// Utility function to apply an action to all .
///
/// The action to execute on each elements.
- private readonly void ForEachDestination(SpanAction action)
+ private readonly void ForEachDestination(SplitterDestinationAction action)
{
- Span temp = Destinations;
+ SplitterDestination temp = Destination;
int i = 0;
while (true)
{
- if (temp.IsEmpty)
+ if (temp.IsNull)
{
break;
}
- Span next = temp[0].Next;
+ SplitterDestination next = temp.Next;
- action.Invoke(temp, i++);
+ action(temp, i++);
temp = next;
}
@@ -142,9 +149,9 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
{
input.ReadLittleEndian(out int destinationId);
- Memory destination = context.GetDestinationMemory(destinationId);
+ SplitterDestination destination = context.GetDestination(destinationId);
- SetDestination(ref destination.Span[0]);
+ SetDestination(destination);
DestinationCount = destinationCount;
@@ -152,9 +159,9 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
{
input.ReadLittleEndian(out destinationId);
- Memory nextDestination = context.GetDestinationMemory(destinationId);
+ SplitterDestination nextDestination = context.GetDestination(destinationId);
- destination.Span[0].Link(ref nextDestination.Span[0]);
+ destination.Link(nextDestination);
destination = nextDestination;
}
}
@@ -174,16 +181,21 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
}
///
- /// Set the head of the linked list of .
+ /// Set the head of the linked list of .
///
- /// A reference to a .
- public void SetDestination(ref SplitterDestination newValue)
+ /// New destination value.
+ public void SetDestination(SplitterDestination newValue)
{
unsafe
{
- fixed (SplitterDestination* newValuePtr = &newValue)
+ fixed (SplitterDestinationVersion1* newValuePtr = &newValue.GetV1RefOrNull())
{
- _destinationsData = newValuePtr;
+ _destinationDataV1 = newValuePtr;
+ }
+
+ fixed (SplitterDestinationVersion2* newValuePtr = &newValue.GetV2RefOrNull())
+ {
+ _destinationDataV2 = newValuePtr;
}
}
}
@@ -193,19 +205,20 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
///
public readonly void UpdateInternalState()
{
- ForEachDestination((destination, _) => destination[0].UpdateInternalState());
+ ForEachDestination((destination, _) => destination.UpdateInternalState());
}
///
- /// Clear all links from the .
+ /// Clear all links from the .
///
public void ClearLinks()
{
- ForEachDestination((destination, _) => destination[0].Unlink());
+ ForEachDestination((destination, _) => destination.Unlink());
unsafe
{
- _destinationsData = (SplitterDestination*)IntPtr.Zero;
+ _destinationDataV1 = null;
+ _destinationDataV2 = null;
}
}
@@ -219,7 +232,8 @@ namespace Ryujinx.Audio.Renderer.Server.Splitter
{
unsafe
{
- splitter._destinationsData = (SplitterDestination*)IntPtr.Zero;
+ splitter._destinationDataV1 = null;
+ splitter._destinationDataV2 = null;
}
splitter.DestinationCount = 0;
diff --git a/src/Ryujinx.Tests/Audio/Renderer/Server/BehaviourContextTests.cs b/src/Ryujinx.Tests/Audio/Renderer/Server/BehaviourContextTests.cs
index 557581881..3e48a5b4e 100644
--- a/src/Ryujinx.Tests/Audio/Renderer/Server/BehaviourContextTests.cs
+++ b/src/Ryujinx.Tests/Audio/Renderer/Server/BehaviourContextTests.cs
@@ -52,7 +52,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported());
- Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported());
+ Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
+ Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
+ Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.70f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -78,7 +80,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported());
- Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported());
+ Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
+ Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
+ Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.70f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -104,7 +108,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported());
- Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported());
+ Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
+ Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
+ Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.70f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -130,7 +136,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported());
- Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported());
+ Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
+ Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
+ Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.75f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(1, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -156,7 +164,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported());
- Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported());
+ Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
+ Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
+ Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(2, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -182,7 +192,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsFalse(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported());
- Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported());
+ Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
+ Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
+ Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(2, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -208,7 +220,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsFalse(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported());
- Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported());
+ Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
+ Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
+ Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(2, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -234,7 +248,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsFalse(behaviourContext.IsEffectInfoVersion2Supported());
- Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported());
+ Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
+ Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
+ Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(3, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -260,7 +276,9 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsTrue(behaviourContext.IsEffectInfoVersion2Supported());
- Assert.IsFalse(behaviourContext.IsBiquadFilterGroupedOptimizationSupported());
+ Assert.IsFalse(behaviourContext.UseMultiTapBiquadFilterProcessing());
+ Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
+ Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(3, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
@@ -286,11 +304,69 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported());
Assert.IsTrue(behaviourContext.IsEffectInfoVersion2Supported());
- Assert.IsTrue(behaviourContext.IsBiquadFilterGroupedOptimizationSupported());
+ Assert.IsTrue(behaviourContext.UseMultiTapBiquadFilterProcessing());
+ Assert.IsFalse(behaviourContext.IsNewEffectChannelMappingSupported());
+ Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
Assert.AreEqual(4, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
Assert.AreEqual(2, behaviourContext.GetPerformanceMetricsDataFormat());
}
+
+ [Test]
+ public void TestRevision11()
+ {
+ BehaviourContext behaviourContext = new();
+
+ behaviourContext.SetUserRevision(BehaviourContext.BaseRevisionMagic + BehaviourContext.Revision11);
+
+ Assert.IsTrue(behaviourContext.IsAdpcmLoopContextBugFixed());
+ Assert.IsTrue(behaviourContext.IsSplitterSupported());
+ Assert.IsTrue(behaviourContext.IsLongSizePreDelaySupported());
+ Assert.IsTrue(behaviourContext.IsAudioUsbDeviceOutputSupported());
+ Assert.IsTrue(behaviourContext.IsFlushVoiceWaveBuffersSupported());
+ Assert.IsTrue(behaviourContext.IsSplitterBugFixed());
+ Assert.IsTrue(behaviourContext.IsElapsedFrameCountSupported());
+ Assert.IsTrue(behaviourContext.IsDecodingBehaviourFlagSupported());
+ Assert.IsTrue(behaviourContext.IsBiquadFilterEffectStateClearBugFixed());
+ Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
+ Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported());
+ Assert.IsTrue(behaviourContext.IsEffectInfoVersion2Supported());
+ Assert.IsTrue(behaviourContext.UseMultiTapBiquadFilterProcessing());
+ Assert.IsTrue(behaviourContext.IsNewEffectChannelMappingSupported());
+ Assert.IsFalse(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
+
+ Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
+ Assert.AreEqual(5, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
+ Assert.AreEqual(2, behaviourContext.GetPerformanceMetricsDataFormat());
+ }
+
+ [Test]
+ public void TestRevision12()
+ {
+ BehaviourContext behaviourContext = new();
+
+ behaviourContext.SetUserRevision(BehaviourContext.BaseRevisionMagic + BehaviourContext.Revision12);
+
+ Assert.IsTrue(behaviourContext.IsAdpcmLoopContextBugFixed());
+ Assert.IsTrue(behaviourContext.IsSplitterSupported());
+ Assert.IsTrue(behaviourContext.IsLongSizePreDelaySupported());
+ Assert.IsTrue(behaviourContext.IsAudioUsbDeviceOutputSupported());
+ Assert.IsTrue(behaviourContext.IsFlushVoiceWaveBuffersSupported());
+ Assert.IsTrue(behaviourContext.IsSplitterBugFixed());
+ Assert.IsTrue(behaviourContext.IsElapsedFrameCountSupported());
+ Assert.IsTrue(behaviourContext.IsDecodingBehaviourFlagSupported());
+ Assert.IsTrue(behaviourContext.IsBiquadFilterEffectStateClearBugFixed());
+ Assert.IsTrue(behaviourContext.IsMixInParameterDirtyOnlyUpdateSupported());
+ Assert.IsTrue(behaviourContext.IsWaveBufferVersion2Supported());
+ Assert.IsTrue(behaviourContext.IsEffectInfoVersion2Supported());
+ Assert.IsTrue(behaviourContext.UseMultiTapBiquadFilterProcessing());
+ Assert.IsTrue(behaviourContext.IsNewEffectChannelMappingSupported());
+ Assert.IsTrue(behaviourContext.IsBiquadFilterParameterForSplitterEnabled());
+
+ Assert.AreEqual(0.80f, behaviourContext.GetAudioRendererProcessingTimeLimit());
+ Assert.AreEqual(5, behaviourContext.GetCommandProcessingTimeEstimatorVersion());
+ Assert.AreEqual(2, behaviourContext.GetPerformanceMetricsDataFormat());
+ }
}
}
diff --git a/src/Ryujinx.Tests/Audio/Renderer/Server/SplitterDestinationTests.cs b/src/Ryujinx.Tests/Audio/Renderer/Server/SplitterDestinationTests.cs
index ad974aab1..80b801336 100644
--- a/src/Ryujinx.Tests/Audio/Renderer/Server/SplitterDestinationTests.cs
+++ b/src/Ryujinx.Tests/Audio/Renderer/Server/SplitterDestinationTests.cs
@@ -9,7 +9,8 @@ namespace Ryujinx.Tests.Audio.Renderer.Server
[Test]
public void EnsureTypeSize()
{
- Assert.AreEqual(0xE0, Unsafe.SizeOf());
+ Assert.AreEqual(0xE0, Unsafe.SizeOf());
+ Assert.AreEqual(0x110, Unsafe.SizeOf());
}
}
}
From 8f51938e2b22ee438ce8f849cc9258026ec5da29 Mon Sep 17 00:00:00 2001
From: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
Date: Fri, 17 May 2024 21:58:03 +0200
Subject: [PATCH 022/109] Disable keyboard controller input while swkbd is open
(foreground) (second attempt) (#6808)
* Block input updates while swkbd is open in foreground mode
* Flush internal driver state before unblocking input updates
* Rename Flush to Clear and remove unnecessary attribute
* Clear the driver state only if the GamepadDriver isn't null
---
src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs | 5 +++++
src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs | 3 +++
src/Ryujinx.Input/HLE/NpadManager.cs | 5 +++++
src/Ryujinx.Input/IGamepadDriver.cs | 6 ++++++
src/Ryujinx/Input/AvaloniaKeyboard.cs | 2 +-
src/Ryujinx/Input/AvaloniaKeyboardDriver.cs | 2 +-
src/Ryujinx/UI/Applet/AvaHostUIHandler.cs | 2 ++
7 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs b/src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs
index e502254be..bd71c7933 100644
--- a/src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs
+++ b/src/Ryujinx.Gtk3/Input/GTK3/GTK3KeyboardDriver.cs
@@ -81,6 +81,11 @@ namespace Ryujinx.Input.GTK3
return _pressedKeys.Contains(nativeKey);
}
+ public void Clear()
+ {
+ _pressedKeys.Clear();
+ }
+
public IGamepad GetGamepad(string id)
{
if (!_keyboardIdentifers[0].Equals(id))
diff --git a/src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs b/src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs
index 1d918d21b..b3f509a09 100644
--- a/src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs
+++ b/src/Ryujinx.Gtk3/UI/Applet/GtkHostUIHandler.cs
@@ -107,6 +107,8 @@ namespace Ryujinx.UI.Applet
swkbdDialog.SetInputLengthValidation(args.StringLengthMin, args.StringLengthMax);
swkbdDialog.SetInputValidation(args.KeyboardMode);
+ ((MainWindow)_parent).RendererWidget.NpadManager.BlockInputUpdates();
+
if (swkbdDialog.Run() == (int)ResponseType.Ok)
{
inputText = swkbdDialog.InputEntry.Text;
@@ -128,6 +130,7 @@ namespace Ryujinx.UI.Applet
});
dialogCloseEvent.WaitOne();
+ ((MainWindow)_parent).RendererWidget.NpadManager.UnblockInputUpdates();
userText = error ? null : inputText;
diff --git a/src/Ryujinx.Input/HLE/NpadManager.cs b/src/Ryujinx.Input/HLE/NpadManager.cs
index 4c7bb8b7a..1bc54d694 100644
--- a/src/Ryujinx.Input/HLE/NpadManager.cs
+++ b/src/Ryujinx.Input/HLE/NpadManager.cs
@@ -174,6 +174,11 @@ namespace Ryujinx.Input.HLE
{
lock (_lock)
{
+ foreach (InputConfig inputConfig in _inputConfig)
+ {
+ _controllers[(int)inputConfig.PlayerIndex].GamepadDriver?.Clear();
+ }
+
_blockInputUpdates = false;
}
}
diff --git a/src/Ryujinx.Input/IGamepadDriver.cs b/src/Ryujinx.Input/IGamepadDriver.cs
index 67b01c26c..625c3e694 100644
--- a/src/Ryujinx.Input/IGamepadDriver.cs
+++ b/src/Ryujinx.Input/IGamepadDriver.cs
@@ -33,5 +33,11 @@ namespace Ryujinx.Input
/// The unique id of the gamepad
/// An instance of associated to the gamepad id given or null if not found
IGamepad GetGamepad(string id);
+
+ ///
+ /// Clear the internal state of the driver.
+ ///
+ /// Does nothing by default.
+ void Clear() { }
}
}
diff --git a/src/Ryujinx/Input/AvaloniaKeyboard.cs b/src/Ryujinx/Input/AvaloniaKeyboard.cs
index fbaaaabab..ff88de79e 100644
--- a/src/Ryujinx/Input/AvaloniaKeyboard.cs
+++ b/src/Ryujinx/Input/AvaloniaKeyboard.cs
@@ -195,7 +195,7 @@ namespace Ryujinx.Ava.Input
public void Clear()
{
- _driver?.ResetKeys();
+ _driver?.Clear();
}
public void Dispose() { }
diff --git a/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs b/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs
index e9e71b99b..9f87e821a 100644
--- a/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs
+++ b/src/Ryujinx/Input/AvaloniaKeyboardDriver.cs
@@ -94,7 +94,7 @@ namespace Ryujinx.Ava.Input
return _pressedKeys.Contains(nativeKey);
}
- public void ResetKeys()
+ public void Clear()
{
_pressedKeys.Clear();
}
diff --git a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
index 4bcc35a7a..4bcf8eb94 100644
--- a/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
+++ b/src/Ryujinx/UI/Applet/AvaHostUIHandler.cs
@@ -122,6 +122,7 @@ namespace Ryujinx.Ava.UI.Applet
{
try
{
+ _parent.ViewModel.AppHost.NpadManager.BlockInputUpdates();
var response = await SwkbdAppletDialog.ShowInputDialog(LocaleManager.Instance[LocaleKeys.SoftwareKeyboard], args);
if (response.Result == UserResult.Ok)
@@ -143,6 +144,7 @@ namespace Ryujinx.Ava.UI.Applet
});
dialogCloseEvent.WaitOne();
+ _parent.ViewModel.AppHost.NpadManager.UnblockInputUpdates();
userText = error ? null : inputText;
From 2f427deb672cfae9f5d607da77086b75720fe416 Mon Sep 17 00:00:00 2001
From: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
Date: Sat, 18 May 2024 01:11:30 +0200
Subject: [PATCH 023/109] Fix another NullReferenceException (#6826)
---
src/Ryujinx.Input/HLE/NpadManager.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/Ryujinx.Input/HLE/NpadManager.cs b/src/Ryujinx.Input/HLE/NpadManager.cs
index 1bc54d694..1dc87358d 100644
--- a/src/Ryujinx.Input/HLE/NpadManager.cs
+++ b/src/Ryujinx.Input/HLE/NpadManager.cs
@@ -176,7 +176,7 @@ namespace Ryujinx.Input.HLE
{
foreach (InputConfig inputConfig in _inputConfig)
{
- _controllers[(int)inputConfig.PlayerIndex].GamepadDriver?.Clear();
+ _controllers[(int)inputConfig.PlayerIndex]?.GamepadDriver?.Clear();
}
_blockInputUpdates = false;
From eb1ce41b00e415fe84537bc872ddbf13996055d5 Mon Sep 17 00:00:00 2001
From: riperiperi
Date: Sun, 19 May 2024 20:53:37 +0100
Subject: [PATCH 024/109] GPU: Migrate buffers on GPU project, pre-emptively
flush device local mappings (#6794)
* GPU: Migrate buffers on GPU project, pre-emptively flush device local mappings
Essentially retreading #4540, but it's on the GPU project now instead of the backend. This allows us to have a lot more control + knowledge of where the buffer backing has been changed and allows us to pre-emptively flush pages to host memory for quicker readback. It will allow us to do other stuff in the future, but we'll get there when we get there.
Performance greatly improved in Hyrule Warriors: Age of Calamity. Performance notably improved in TOTK (average). Performance for BOTW restored to how it was before #4911, perhaps a bit better.
- Rewrites a bunch of buffer migration stuff. Might want to tighten up how dispose stuff works.
- Fixed an issue where the copy for texture pre-flush would happen _after_ the syncpoint.
TODO: remove a page from pre-flush if it isn't flushed after a certain number of copies.
* Add copy deactivation
* Fix dependent virtual buffers
* Remove logging
* Fix format issues (maybe)
* Vulkan: Remove backing swap
* Add explicit memory access types for most buffers
* Fix typo
* Add device local force expiry, change buffer inheritance behaviour
* General cleanup, OGL fix
* BufferPreFlush comments
* BufferBackingState comments
* Add an extra precaution to BufferMigration
This is very unlikely, but it's important to cover loose ends like this.
* Address some feedback
* Docs
---
src/Ryujinx.Graphics.GAL/BufferAccess.cs | 11 +-
src/Ryujinx.Graphics.GAL/Capabilities.cs | 3 +
src/Ryujinx.Graphics.GAL/IRenderer.cs | 1 -
.../Multithreading/CommandHelper.cs | 1 -
.../Multithreading/CommandType.cs | 1 -
.../Commands/Renderer/CreateBufferCommand.cs | 31 --
.../Multithreading/ThreadedRenderer.cs | 9 -
src/Ryujinx.Graphics.GAL/SystemMemoryType.cs | 29 ++
.../Engine/MME/MacroHLE.cs | 5 +-
.../Threed/ComputeDraw/VtgAsComputeContext.cs | 8 +-
.../Threed/ComputeDraw/VtgAsComputeState.cs | 7 +-
.../Engine/Threed/DrawManager.cs | 6 +-
src/Ryujinx.Graphics.Gpu/GpuContext.cs | 5 +-
.../Image/TextureBindingsArrayCache.cs | 8 +-
.../Image/TextureGroup.cs | 2 +-
src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs | 174 ++++++++++-
.../Memory/BufferBackingState.cs | 294 +++++++++++++++++
.../Memory/BufferCache.cs | 118 ++++---
.../Memory/BufferManager.cs | 47 +--
.../Memory/BufferMigration.cs | 250 +++++++++++----
.../Memory/BufferModifiedRangeList.cs | 138 +++++---
.../Memory/BufferPreFlush.cs | 295 ++++++++++++++++++
.../Memory/BufferStage.cs | 99 ++++++
src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs | 10 +-
src/Ryujinx.Graphics.Vulkan/BufferHolder.cs | 227 +-------------
src/Ryujinx.Graphics.Vulkan/BufferManager.cs | 37 +--
src/Ryujinx.Graphics.Vulkan/EnumConversion.cs | 12 +-
src/Ryujinx.Graphics.Vulkan/PipelineFull.cs | 16 -
src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs | 21 +-
29 files changed, 1342 insertions(+), 523 deletions(-)
delete mode 100644 src/Ryujinx.Graphics.GAL/Multithreading/Commands/Renderer/CreateBufferCommand.cs
create mode 100644 src/Ryujinx.Graphics.GAL/SystemMemoryType.cs
create mode 100644 src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs
create mode 100644 src/Ryujinx.Graphics.Gpu/Memory/BufferPreFlush.cs
create mode 100644 src/Ryujinx.Graphics.Gpu/Memory/BufferStage.cs
diff --git a/src/Ryujinx.Graphics.GAL/BufferAccess.cs b/src/Ryujinx.Graphics.GAL/BufferAccess.cs
index faefa5188..1e7736f8f 100644
--- a/src/Ryujinx.Graphics.GAL/BufferAccess.cs
+++ b/src/Ryujinx.Graphics.GAL/BufferAccess.cs
@@ -6,8 +6,13 @@ namespace Ryujinx.Graphics.GAL
public enum BufferAccess
{
Default = 0,
- FlushPersistent = 1 << 0,
- Stream = 1 << 1,
- SparseCompatible = 1 << 2,
+ HostMemory = 1,
+ DeviceMemory = 2,
+ DeviceMemoryMapped = 3,
+
+ MemoryTypeMask = 0xf,
+
+ Stream = 1 << 4,
+ SparseCompatible = 1 << 5,
}
}
diff --git a/src/Ryujinx.Graphics.GAL/Capabilities.cs b/src/Ryujinx.Graphics.GAL/Capabilities.cs
index 779ce5b5d..d758586ae 100644
--- a/src/Ryujinx.Graphics.GAL/Capabilities.cs
+++ b/src/Ryujinx.Graphics.GAL/Capabilities.cs
@@ -6,6 +6,7 @@ namespace Ryujinx.Graphics.GAL
{
public readonly TargetApi Api;
public readonly string VendorName;
+ public readonly SystemMemoryType MemoryType;
public readonly bool HasFrontFacingBug;
public readonly bool HasVectorIndexingBug;
@@ -66,6 +67,7 @@ namespace Ryujinx.Graphics.GAL
public Capabilities(
TargetApi api,
string vendorName,
+ SystemMemoryType memoryType,
bool hasFrontFacingBug,
bool hasVectorIndexingBug,
bool needsFragmentOutputSpecialization,
@@ -120,6 +122,7 @@ namespace Ryujinx.Graphics.GAL
{
Api = api;
VendorName = vendorName;
+ MemoryType = memoryType;
HasFrontFacingBug = hasFrontFacingBug;
HasVectorIndexingBug = hasVectorIndexingBug;
NeedsFragmentOutputSpecialization = needsFragmentOutputSpecialization;
diff --git a/src/Ryujinx.Graphics.GAL/IRenderer.cs b/src/Ryujinx.Graphics.GAL/IRenderer.cs
index a3466e396..85d0bd729 100644
--- a/src/Ryujinx.Graphics.GAL/IRenderer.cs
+++ b/src/Ryujinx.Graphics.GAL/IRenderer.cs
@@ -17,7 +17,6 @@ namespace Ryujinx.Graphics.GAL
void BackgroundContextAction(Action action, bool alwaysBackground = false);
BufferHandle CreateBuffer(int size, BufferAccess access = BufferAccess.Default);
- BufferHandle CreateBuffer(int size, BufferAccess access, BufferHandle storageHint);
BufferHandle CreateBuffer(nint pointer, int size);
BufferHandle CreateBufferSparse(ReadOnlySpan storageBuffers);
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs b/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
index fd2919be4..23f1a64ef 100644
--- a/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
@@ -44,7 +44,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
}
Register(CommandType.Action);
- Register(CommandType.CreateBuffer);
Register(CommandType.CreateBufferAccess);
Register(CommandType.CreateBufferSparse);
Register(CommandType.CreateHostBuffer);
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs b/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs
index a5e7336cd..f95aab05b 100644
--- a/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs
@@ -3,7 +3,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
enum CommandType : byte
{
Action,
- CreateBuffer,
CreateBufferAccess,
CreateBufferSparse,
CreateHostBuffer,
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Renderer/CreateBufferCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Renderer/CreateBufferCommand.cs
deleted file mode 100644
index 60a6e4bf4..000000000
--- a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/Renderer/CreateBufferCommand.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-namespace Ryujinx.Graphics.GAL.Multithreading.Commands.Renderer
-{
- struct CreateBufferCommand : IGALCommand, IGALCommand
- {
- public readonly CommandType CommandType => CommandType.CreateBuffer;
- private BufferHandle _threadedHandle;
- private int _size;
- private BufferAccess _access;
- private BufferHandle _storageHint;
-
- public void Set(BufferHandle threadedHandle, int size, BufferAccess access, BufferHandle storageHint)
- {
- _threadedHandle = threadedHandle;
- _size = size;
- _access = access;
- _storageHint = storageHint;
- }
-
- public static void Run(ref CreateBufferCommand command, ThreadedRenderer threaded, IRenderer renderer)
- {
- BufferHandle hint = BufferHandle.Null;
-
- if (command._storageHint != BufferHandle.Null)
- {
- hint = threaded.Buffers.MapBuffer(command._storageHint);
- }
-
- threaded.Buffers.AssignBuffer(command._threadedHandle, renderer.CreateBuffer(command._size, command._access, hint));
- }
- }
-}
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs
index 5e17bcd2c..cc3d2e5c1 100644
--- a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedRenderer.cs
@@ -272,15 +272,6 @@ namespace Ryujinx.Graphics.GAL.Multithreading
return handle;
}
- public BufferHandle CreateBuffer(int size, BufferAccess access, BufferHandle storageHint)
- {
- BufferHandle handle = Buffers.CreateBufferHandle();
- New().Set(handle, size, access, storageHint);
- QueueCommand();
-
- return handle;
- }
-
public BufferHandle CreateBuffer(nint pointer, int size)
{
BufferHandle handle = Buffers.CreateBufferHandle();
diff --git a/src/Ryujinx.Graphics.GAL/SystemMemoryType.cs b/src/Ryujinx.Graphics.GAL/SystemMemoryType.cs
new file mode 100644
index 000000000..532921298
--- /dev/null
+++ b/src/Ryujinx.Graphics.GAL/SystemMemoryType.cs
@@ -0,0 +1,29 @@
+namespace Ryujinx.Graphics.GAL
+{
+ public enum SystemMemoryType
+ {
+ ///
+ /// The backend manages the ownership of memory. This mode never supports host imported memory.
+ ///
+ BackendManaged,
+
+ ///
+ /// Device memory has similar performance to host memory, usually because it's shared between CPU/GPU.
+ /// Use host memory whenever possible.
+ ///
+ UnifiedMemory,
+
+ ///
+ /// GPU storage to host memory goes though a slow interconnect, but it would still be preferable to use it if the data is flushed back often.
+ /// Assumes constant buffer access to host memory is rather fast.
+ ///
+ DedicatedMemory,
+
+ ///
+ /// GPU storage to host memory goes though a slow interconnect, that is very slow when doing access from storage.
+ /// When frequently accessed, copy buffers to host memory using DMA.
+ /// Assumes constant buffer access to host memory is rather fast.
+ ///
+ DedicatedMemorySlowStorage
+ }
+}
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLE.cs b/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLE.cs
index 7f3772f44..475d1ee4e 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLE.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/MME/MacroHLE.cs
@@ -5,6 +5,7 @@ using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Engine.GPFifo;
using Ryujinx.Graphics.Gpu.Engine.Threed;
using Ryujinx.Graphics.Gpu.Engine.Types;
+using Ryujinx.Graphics.Gpu.Memory;
using Ryujinx.Memory.Range;
using System;
using System.Collections.Generic;
@@ -495,8 +496,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.MME
ulong indirectBufferSize = (ulong)maxDrawCount * (ulong)stride;
- MultiRange indirectBufferRange = bufferCache.TranslateAndCreateMultiBuffers(_processor.MemoryManager, indirectBufferGpuVa, indirectBufferSize);
- MultiRange parameterBufferRange = bufferCache.TranslateAndCreateMultiBuffers(_processor.MemoryManager, parameterBufferGpuVa, 4);
+ MultiRange indirectBufferRange = bufferCache.TranslateAndCreateMultiBuffers(_processor.MemoryManager, indirectBufferGpuVa, indirectBufferSize, BufferStage.Indirect);
+ MultiRange parameterBufferRange = bufferCache.TranslateAndCreateMultiBuffers(_processor.MemoryManager, parameterBufferGpuVa, 4, BufferStage.Indirect);
_processor.ThreedClass.DrawIndirect(
topology,
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs
index f9cb40b0d..6de50fb2e 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs
@@ -438,7 +438,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
ReadOnlySpan dataBytes = MemoryMarshal.Cast(data);
- BufferHandle buffer = _context.Renderer.CreateBuffer(dataBytes.Length);
+ BufferHandle buffer = _context.Renderer.CreateBuffer(dataBytes.Length, BufferAccess.DeviceMemory);
_context.Renderer.SetBufferData(buffer, 0, dataBytes);
return new IndexBuffer(buffer, count, dataBytes.Length);
@@ -529,7 +529,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
{
if (_dummyBuffer == BufferHandle.Null)
{
- _dummyBuffer = _context.Renderer.CreateBuffer(DummyBufferSize);
+ _dummyBuffer = _context.Renderer.CreateBuffer(DummyBufferSize, BufferAccess.DeviceMemory);
_context.Renderer.Pipeline.ClearBuffer(_dummyBuffer, 0, DummyBufferSize, 0);
}
@@ -550,7 +550,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
_context.Renderer.DeleteBuffer(_sequentialIndexBuffer);
}
- _sequentialIndexBuffer = _context.Renderer.CreateBuffer(count * sizeof(uint));
+ _sequentialIndexBuffer = _context.Renderer.CreateBuffer(count * sizeof(uint), BufferAccess.DeviceMemory);
_sequentialIndexBufferCount = count;
Span data = new int[count];
@@ -583,7 +583,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
_context.Renderer.DeleteBuffer(buffer.Handle);
}
- buffer.Handle = _context.Renderer.CreateBuffer(newSize);
+ buffer.Handle = _context.Renderer.CreateBuffer(newSize, BufferAccess.DeviceMemory);
buffer.Size = newSize;
}
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs
index 6324e6a15..73682866b 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs
@@ -3,6 +3,7 @@ using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Gpu.Engine.Types;
using Ryujinx.Graphics.Gpu.Image;
+using Ryujinx.Graphics.Gpu.Memory;
using Ryujinx.Graphics.Gpu.Shader;
using Ryujinx.Graphics.Shader;
using Ryujinx.Graphics.Shader.Translation;
@@ -370,7 +371,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
{
var memoryManager = _channel.MemoryManager;
- BufferRange range = memoryManager.Physical.BufferCache.GetBufferRange(memoryManager.GetPhysicalRegions(address, size));
+ BufferRange range = memoryManager.Physical.BufferCache.GetBufferRange(memoryManager.GetPhysicalRegions(address, size), BufferStage.VertexBuffer);
ITexture bufferTexture = _vacContext.EnsureBufferTexture(index + 2, format);
bufferTexture.SetStorage(range);
@@ -412,7 +413,9 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw
var memoryManager = _channel.MemoryManager;
ulong misalign = address & ((ulong)_context.Capabilities.TextureBufferOffsetAlignment - 1);
- BufferRange range = memoryManager.Physical.BufferCache.GetBufferRange(memoryManager.GetPhysicalRegions(address + indexOffset - misalign, size + misalign));
+ BufferRange range = memoryManager.Physical.BufferCache.GetBufferRange(
+ memoryManager.GetPhysicalRegions(address + indexOffset - misalign, size + misalign),
+ BufferStage.IndexBuffer);
misalignedOffset = (int)misalign >> shift;
SetIndexBufferTexture(reservations, range, format);
diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs
index d8de14de0..56ef64c6e 100644
--- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs
+++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/DrawManager.cs
@@ -684,8 +684,8 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
if (hasCount)
{
- var indirectBuffer = memory.BufferCache.GetBufferRange(indirectBufferRange);
- var parameterBuffer = memory.BufferCache.GetBufferRange(parameterBufferRange);
+ var indirectBuffer = memory.BufferCache.GetBufferRange(indirectBufferRange, BufferStage.Indirect);
+ var parameterBuffer = memory.BufferCache.GetBufferRange(parameterBufferRange, BufferStage.Indirect);
if (indexed)
{
@@ -698,7 +698,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed
}
else
{
- var indirectBuffer = memory.BufferCache.GetBufferRange(indirectBufferRange);
+ var indirectBuffer = memory.BufferCache.GetBufferRange(indirectBufferRange, BufferStage.Indirect);
if (indexed)
{
diff --git a/src/Ryujinx.Graphics.Gpu/GpuContext.cs b/src/Ryujinx.Graphics.Gpu/GpuContext.cs
index 53ea8cb27..048d32fb7 100644
--- a/src/Ryujinx.Graphics.Gpu/GpuContext.cs
+++ b/src/Ryujinx.Graphics.Gpu/GpuContext.cs
@@ -393,17 +393,18 @@ namespace Ryujinx.Graphics.Gpu
if (force || _pendingSync || (syncpoint && SyncpointActions.Count > 0))
{
- Renderer.CreateSync(SyncNumber, strict);
-
foreach (var action in SyncActions)
{
action.SyncPreAction(syncpoint);
}
+
foreach (var action in SyncpointActions)
{
action.SyncPreAction(syncpoint);
}
+ Renderer.CreateSync(SyncNumber, strict);
+
SyncNumber++;
SyncActions.RemoveAll(action => action.SyncAction(syncpoint));
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs
index 7e486e0a8..a54d07000 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs
@@ -708,11 +708,11 @@ namespace Ryujinx.Graphics.Gpu.Image
format = texture.Format;
}
- _channel.BufferManager.SetBufferTextureStorage(entry.ImageArray, hostTexture, texture.Range, bindingInfo, index, format);
+ _channel.BufferManager.SetBufferTextureStorage(stage, entry.ImageArray, hostTexture, texture.Range, bindingInfo, index, format);
}
else
{
- _channel.BufferManager.SetBufferTextureStorage(entry.TextureArray, hostTexture, texture.Range, bindingInfo, index, format);
+ _channel.BufferManager.SetBufferTextureStorage(stage, entry.TextureArray, hostTexture, texture.Range, bindingInfo, index, format);
}
}
else if (isImage)
@@ -921,11 +921,11 @@ namespace Ryujinx.Graphics.Gpu.Image
format = texture.Format;
}
- _channel.BufferManager.SetBufferTextureStorage(entry.ImageArray, hostTexture, texture.Range, bindingInfo, index, format);
+ _channel.BufferManager.SetBufferTextureStorage(stage, entry.ImageArray, hostTexture, texture.Range, bindingInfo, index, format);
}
else
{
- _channel.BufferManager.SetBufferTextureStorage(entry.TextureArray, hostTexture, texture.Range, bindingInfo, index, format);
+ _channel.BufferManager.SetBufferTextureStorage(stage, entry.TextureArray, hostTexture, texture.Range, bindingInfo, index, format);
}
}
else if (isImage)
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
index 4e1133d1a..06ca2c599 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureGroup.cs
@@ -645,7 +645,7 @@ namespace Ryujinx.Graphics.Gpu.Image
}
else
{
- _flushBuffer = _context.Renderer.CreateBuffer((int)Storage.Size, BufferAccess.FlushPersistent);
+ _flushBuffer = _context.Renderer.CreateBuffer((int)Storage.Size, BufferAccess.HostMemory);
_flushBufferImported = false;
}
diff --git a/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs b/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs
index d293060b5..e060e0b4f 100644
--- a/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs
+++ b/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs
@@ -10,6 +10,8 @@ using System.Threading;
namespace Ryujinx.Graphics.Gpu.Memory
{
+ delegate void BufferFlushAction(ulong address, ulong size, ulong syncNumber);
+
///
/// Buffer, used to store vertex and index data, uniform and storage buffers, and others.
///
@@ -23,7 +25,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
///
/// Host buffer handle.
///
- public BufferHandle Handle { get; }
+ public BufferHandle Handle { get; private set; }
///
/// Start address of the buffer in guest memory.
@@ -60,6 +62,17 @@ namespace Ryujinx.Graphics.Gpu.Memory
///
private BufferModifiedRangeList _modifiedRanges = null;
+ ///
+ /// A structure that is used to flush buffer data back to a host mapped buffer for cached readback.
+ /// Only used if the buffer data is explicitly owned by device local memory.
+ ///
+ private BufferPreFlush _preFlush = null;
+
+ ///
+ /// Usage tracking state that determines what type of backing the buffer should use.
+ ///
+ public BufferBackingState BackingState;
+
private readonly MultiRegionHandle _memoryTrackingGranular;
private readonly RegionHandle _memoryTracking;
@@ -87,6 +100,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Physical memory where the buffer is mapped
/// Start address of the buffer
/// Size of the buffer in bytes
+ /// The type of usage that created the buffer
/// Indicates if the buffer can be used in a sparse buffer mapping
/// Buffers which this buffer contains, and will inherit tracking handles from
public Buffer(
@@ -94,6 +108,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
PhysicalMemory physicalMemory,
ulong address,
ulong size,
+ BufferStage stage,
bool sparseCompatible,
IEnumerable baseBuffers = null)
{
@@ -103,9 +118,11 @@ namespace Ryujinx.Graphics.Gpu.Memory
Size = size;
SparseCompatible = sparseCompatible;
- BufferAccess access = sparseCompatible ? BufferAccess.SparseCompatible : BufferAccess.Default;
+ BackingState = new BufferBackingState(_context, this, stage, baseBuffers);
- Handle = context.Renderer.CreateBuffer((int)size, access, baseBuffers?.MaxBy(x => x.Size).Handle ?? BufferHandle.Null);
+ BufferAccess access = BackingState.SwitchAccess(this);
+
+ Handle = context.Renderer.CreateBuffer((int)size, access);
_useGranular = size > GranularBufferThreshold;
@@ -161,6 +178,29 @@ namespace Ryujinx.Graphics.Gpu.Memory
_virtualDependenciesLock = new ReaderWriterLockSlim();
}
+ ///
+ /// Recreates the backing buffer based on the desired access type
+ /// reported by the backing state struct.
+ ///
+ private void ChangeBacking()
+ {
+ BufferAccess access = BackingState.SwitchAccess(this);
+
+ BufferHandle newHandle = _context.Renderer.CreateBuffer((int)Size, access);
+
+ _context.Renderer.Pipeline.CopyBuffer(Handle, newHandle, 0, 0, (int)Size);
+
+ _modifiedRanges?.SelfMigration();
+
+ // If swtiching from device local to host mapped, pre-flushing data no longer makes sense.
+ // This is set to null and disposed when the migration fully completes.
+ _preFlush = null;
+
+ Handle = newHandle;
+
+ _physicalMemory.BufferCache.BufferBackingChanged(this);
+ }
+
///
/// Gets a sub-range from the buffer, from a start address til a page boundary after the given size.
///
@@ -246,6 +286,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
else
{
+ BackingState.RecordSet();
_context.Renderer.SetBufferData(Handle, 0, _physicalMemory.GetSpan(Address, (int)Size));
CopyToDependantVirtualBuffers();
}
@@ -283,15 +324,35 @@ namespace Ryujinx.Graphics.Gpu.Memory
_modifiedRanges ??= new BufferModifiedRangeList(_context, this, Flush);
}
+ ///
+ /// Checks if a backing change is deemed necessary from the given usage.
+ /// If it is, queues a backing change to happen on the next sync action.
+ ///
+ /// Buffer stage that can change backing type
+ private void TryQueueBackingChange(BufferStage stage)
+ {
+ if (BackingState.ShouldChangeBacking(stage))
+ {
+ if (!_syncActionRegistered)
+ {
+ _context.RegisterSyncAction(this);
+ _syncActionRegistered = true;
+ }
+ }
+ }
+
///
/// Signal that the given region of the buffer has been modified.
///
/// The start address of the modified region
/// The size of the modified region
- public void SignalModified(ulong address, ulong size)
+ /// Buffer stage that triggered the modification
+ public void SignalModified(ulong address, ulong size, BufferStage stage)
{
EnsureRangeList();
+ TryQueueBackingChange(stage);
+
_modifiedRanges.SignalModified(address, size);
if (!_syncActionRegistered)
@@ -311,6 +372,37 @@ namespace Ryujinx.Graphics.Gpu.Memory
_modifiedRanges?.Clear(address, size);
}
+ ///
+ /// Action to be performed immediately before sync is created.
+ /// This will copy any buffer ranges designated for pre-flushing.
+ ///
+ /// True if the action is a guest syncpoint
+ public void SyncPreAction(bool syncpoint)
+ {
+ if (_referenceCount == 0)
+ {
+ return;
+ }
+
+ if (BackingState.ShouldChangeBacking())
+ {
+ ChangeBacking();
+ }
+
+ if (BackingState.IsDeviceLocal)
+ {
+ _preFlush ??= new BufferPreFlush(_context, this, FlushImpl);
+
+ if (_preFlush.ShouldCopy)
+ {
+ _modifiedRanges?.GetRangesAtSync(Address, Size, _context.SyncNumber, (address, size) =>
+ {
+ _preFlush.CopyModified(address, size);
+ });
+ }
+ }
+ }
+
///
/// Action to be performed when a syncpoint is reached after modification.
/// This will register read/write tracking to flush the buffer from GPU when its memory is used.
@@ -466,6 +558,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Size of the modified region
private void LoadRegion(ulong mAddress, ulong mSize)
{
+ BackingState.RecordSet();
+
int offset = (int)(mAddress - Address);
_context.Renderer.SetBufferData(Handle, offset, _physicalMemory.GetSpan(mAddress, (int)mSize));
@@ -539,18 +633,84 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Flushes a range of the buffer.
/// This writes the range data back into guest memory.
///
+ /// Buffer handle to flush data from
/// Start address of the range
/// Size in bytes of the range
- public void Flush(ulong address, ulong size)
+ private void FlushImpl(BufferHandle handle, ulong address, ulong size)
{
int offset = (int)(address - Address);
- using PinnedSpan data = _context.Renderer.GetBufferData(Handle, offset, (int)size);
+ using PinnedSpan data = _context.Renderer.GetBufferData(handle, offset, (int)size);
// TODO: When write tracking shaders, they will need to be aware of changes in overlapping buffers.
_physicalMemory.WriteUntracked(address, CopyFromDependantVirtualBuffers(data.Get(), address, size));
}
+ ///
+ /// Flushes a range of the buffer.
+ /// This writes the range data back into guest memory.
+ ///
+ /// Start address of the range
+ /// Size in bytes of the range
+ private void FlushImpl(ulong address, ulong size)
+ {
+ FlushImpl(Handle, address, size);
+ }
+
+ ///
+ /// Flushes a range of the buffer from the most optimal source.
+ /// This writes the range data back into guest memory.
+ ///
+ /// Start address of the range
+ /// Size in bytes of the range
+ /// Sync number waited for before flushing the data
+ public void Flush(ulong address, ulong size, ulong syncNumber)
+ {
+ BackingState.RecordFlush();
+
+ BufferPreFlush preFlush = _preFlush;
+
+ if (preFlush != null)
+ {
+ preFlush.FlushWithAction(address, size, syncNumber);
+ }
+ else
+ {
+ FlushImpl(address, size);
+ }
+ }
+ ///
+ /// Gets an action that disposes the backing buffer using its current handle.
+ /// Useful for deleting an old copy of the buffer after the handle changes.
+ ///
+ /// An action that flushes data from the specified range, using the buffer handle at the time the method is generated
+ public Action GetSnapshotDisposeAction()
+ {
+ BufferHandle handle = Handle;
+ BufferPreFlush preFlush = _preFlush;
+
+ return () =>
+ {
+ _context.Renderer.DeleteBuffer(handle);
+ preFlush?.Dispose();
+ };
+ }
+
+ ///
+ /// Gets an action that flushes a range of the buffer using its current handle.
+ /// Useful for flushing data from old copies of the buffer after the handle changes.
+ ///
+ /// An action that flushes data from the specified range, using the buffer handle at the time the method is generated
+ public BufferFlushAction GetSnapshotFlushAction()
+ {
+ BufferHandle handle = Handle;
+
+ return (ulong address, ulong size, ulong _) =>
+ {
+ FlushImpl(handle, address, size);
+ };
+ }
+
///
/// Align a given address and size region to page boundaries.
///
@@ -857,6 +1017,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
_modifiedRanges?.Clear();
_context.Renderer.DeleteBuffer(Handle);
+ _preFlush?.Dispose();
+ _preFlush = null;
UnmappedSequence++;
}
diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs
new file mode 100644
index 000000000..3f65131e6
--- /dev/null
+++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs
@@ -0,0 +1,294 @@
+using Ryujinx.Graphics.GAL;
+using System;
+using System.Collections.Generic;
+
+namespace Ryujinx.Graphics.Gpu.Memory
+{
+ ///
+ /// Type of backing memory.
+ /// In ascending order of priority when merging multiple buffer backing states.
+ ///
+ internal enum BufferBackingType
+ {
+ HostMemory,
+ DeviceMemory,
+ DeviceMemoryWithFlush
+ }
+
+ ///
+ /// Keeps track of buffer usage to decide what memory heap that buffer memory is placed on.
+ /// Dedicated GPUs prefer certain types of resources to be device local,
+ /// and if we need data to be read back, we might prefer that they're in host memory.
+ ///
+ /// The measurements recorded here compare to a set of heruristics (thresholds and conditions)
+ /// that appear to produce good performance in most software.
+ ///
+ internal struct BufferBackingState
+ {
+ private const int DeviceLocalSizeThreshold = 256 * 1024; // 256kb
+
+ private const int SetCountThreshold = 100;
+ private const int WriteCountThreshold = 50;
+ private const int FlushCountThreshold = 5;
+ private const int DeviceLocalForceExpiry = 100;
+
+ public readonly bool IsDeviceLocal => _activeType != BufferBackingType.HostMemory;
+
+ private readonly SystemMemoryType _systemMemoryType;
+ private BufferBackingType _activeType;
+ private BufferBackingType _desiredType;
+
+ private bool _canSwap;
+
+ private int _setCount;
+ private int _writeCount;
+ private int _flushCount;
+ private int _flushTemp;
+ private int _lastFlushWrite;
+ private int _deviceLocalForceCount;
+
+ private readonly int _size;
+
+ ///
+ /// Initialize the buffer backing state for a given parent buffer.
+ ///
+ /// GPU context
+ /// Parent buffer
+ /// Initial buffer stage
+ /// Buffers to inherit state from
+ public BufferBackingState(GpuContext context, Buffer parent, BufferStage stage, IEnumerable baseBuffers = null)
+ {
+ _size = (int)parent.Size;
+ _systemMemoryType = context.Capabilities.MemoryType;
+
+ // Backend managed is always auto, unified memory is always host.
+ _desiredType = BufferBackingType.HostMemory;
+ _canSwap = _systemMemoryType != SystemMemoryType.BackendManaged && _systemMemoryType != SystemMemoryType.UnifiedMemory;
+
+ if (_canSwap)
+ {
+ // Might want to start certain buffers as being device local,
+ // and the usage might also lock those buffers into being device local.
+
+ BufferStage storageFlags = stage & BufferStage.StorageMask;
+
+ if (parent.Size > DeviceLocalSizeThreshold && baseBuffers == null)
+ {
+ _desiredType = BufferBackingType.DeviceMemory;
+ }
+
+ if (storageFlags != 0)
+ {
+ // Storage buffer bindings may require special treatment.
+
+ var rawStage = stage & BufferStage.StageMask;
+
+ if (rawStage == BufferStage.Fragment)
+ {
+ // Fragment read should start device local.
+
+ _desiredType = BufferBackingType.DeviceMemory;
+
+ if (storageFlags != BufferStage.StorageRead)
+ {
+ // Fragment write should stay device local until the use doesn't happen anymore.
+
+ _deviceLocalForceCount = DeviceLocalForceExpiry;
+ }
+ }
+
+ // TODO: Might be nice to force atomic access to be device local for any stage.
+ }
+
+ if (baseBuffers != null)
+ {
+ foreach (Buffer buffer in baseBuffers)
+ {
+ CombineState(buffer.BackingState);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Combine buffer backing types, selecting the one with highest priority.
+ ///
+ /// First buffer backing type
+ /// Second buffer backing type
+ /// Combined buffer backing type
+ private static BufferBackingType CombineTypes(BufferBackingType left, BufferBackingType right)
+ {
+ return (BufferBackingType)Math.Max((int)left, (int)right);
+ }
+
+ ///
+ /// Combine the state from the given buffer backing state with this one,
+ /// so that the state isn't lost when migrating buffers.
+ ///
+ /// Buffer state to combine into this state
+ private void CombineState(BufferBackingState oldState)
+ {
+ _setCount += oldState._setCount;
+ _writeCount += oldState._writeCount;
+ _flushCount += oldState._flushCount;
+ _flushTemp += oldState._flushTemp;
+ _lastFlushWrite = -1;
+ _deviceLocalForceCount = Math.Max(_deviceLocalForceCount, oldState._deviceLocalForceCount);
+
+ _canSwap &= oldState._canSwap;
+
+ _desiredType = CombineTypes(_desiredType, oldState._desiredType);
+ }
+
+ ///
+ /// Get the buffer access for the desired backing type, and record that type as now being active.
+ ///
+ /// Parent buffer
+ /// Buffer access
+ public BufferAccess SwitchAccess(Buffer parent)
+ {
+ BufferAccess access = parent.SparseCompatible ? BufferAccess.SparseCompatible : BufferAccess.Default;
+
+ bool isBackendManaged = _systemMemoryType == SystemMemoryType.BackendManaged;
+
+ if (!isBackendManaged)
+ {
+ switch (_desiredType)
+ {
+ case BufferBackingType.HostMemory:
+ access |= BufferAccess.HostMemory;
+ break;
+ case BufferBackingType.DeviceMemory:
+ access |= BufferAccess.DeviceMemory;
+ break;
+ case BufferBackingType.DeviceMemoryWithFlush:
+ access |= BufferAccess.DeviceMemoryMapped;
+ break;
+ }
+ }
+
+ _activeType = _desiredType;
+
+ return access;
+ }
+
+ ///
+ /// Record when data has been uploaded to the buffer.
+ ///
+ public void RecordSet()
+ {
+ _setCount++;
+
+ ConsiderUseCounts();
+ }
+
+ ///
+ /// Record when data has been flushed from the buffer.
+ ///
+ public void RecordFlush()
+ {
+ if (_lastFlushWrite != _writeCount)
+ {
+ // If it's on the same page as the last flush, ignore it.
+ _lastFlushWrite = _writeCount;
+ _flushCount++;
+ }
+ }
+
+ ///
+ /// Determine if the buffer backing should be changed.
+ ///
+ /// True if the desired backing type is different from the current type
+ public readonly bool ShouldChangeBacking()
+ {
+ return _desiredType != _activeType;
+ }
+
+ ///
+ /// Determine if the buffer backing should be changed, considering a new use with the given buffer stage.
+ ///
+ /// Buffer stage for the use
+ /// True if the desired backing type is different from the current type
+ public bool ShouldChangeBacking(BufferStage stage)
+ {
+ if (!_canSwap)
+ {
+ return false;
+ }
+
+ BufferStage storageFlags = stage & BufferStage.StorageMask;
+
+ if (storageFlags != 0)
+ {
+ if (storageFlags != BufferStage.StorageRead)
+ {
+ // Storage write.
+ _writeCount++;
+
+ var rawStage = stage & BufferStage.StageMask;
+
+ if (rawStage == BufferStage.Fragment)
+ {
+ // Switch to device memory, swap back only if this use disappears.
+
+ _desiredType = CombineTypes(_desiredType, BufferBackingType.DeviceMemory);
+ _deviceLocalForceCount = DeviceLocalForceExpiry;
+
+ // TODO: Might be nice to force atomic access to be device local for any stage.
+ }
+ }
+
+ ConsiderUseCounts();
+ }
+
+ return _desiredType != _activeType;
+ }
+
+ ///
+ /// Evaluate the current counts to determine what the buffer's desired backing type is.
+ /// This method depends on heuristics devised by testing a variety of software.
+ ///
+ private void ConsiderUseCounts()
+ {
+ if (_canSwap)
+ {
+ if (_writeCount >= WriteCountThreshold || _setCount >= SetCountThreshold || _flushCount >= FlushCountThreshold)
+ {
+ if (_deviceLocalForceCount > 0 && --_deviceLocalForceCount != 0)
+ {
+ // Some buffer usage demanded that the buffer stay device local.
+ // The desired type was selected when this counter was set.
+ }
+ else if (_flushCount > 0 || _flushTemp-- > 0)
+ {
+ // Buffers that flush should ideally be mapped in host address space for easy copies.
+ // If the buffer is large it will do better on GPU memory, as there will be more writes than data flushes (typically individual pages).
+ // If it is small, then it's likely most of the buffer will be flushed so we want it on host memory, as access is cached.
+ _desiredType = _size > DeviceLocalSizeThreshold ? BufferBackingType.DeviceMemoryWithFlush : BufferBackingType.HostMemory;
+ }
+ else if (_writeCount >= WriteCountThreshold)
+ {
+ // Buffers that are written often should ideally be in the device local heap. (Storage buffers)
+ _desiredType = BufferBackingType.DeviceMemory;
+ }
+ else if (_setCount > SetCountThreshold)
+ {
+ // Buffers that have their data set often should ideally be host mapped. (Constant buffers)
+ _desiredType = BufferBackingType.HostMemory;
+ }
+
+ // It's harder for a buffer that is flushed to revert to another type of mapping.
+ if (_flushCount > 0)
+ {
+ _flushTemp = 1000;
+ }
+
+ _lastFlushWrite = -1;
+ _flushCount = 0;
+ _writeCount = 0;
+ _setCount = 0;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs
index c6284780d..66d2cdb62 100644
--- a/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs
+++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs
@@ -107,8 +107,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// GPU memory manager where the buffer is mapped
/// Start GPU virtual address of the buffer
/// Size in bytes of the buffer
+ /// The type of usage that created the buffer
/// Contiguous physical range of the buffer, after address translation
- public MultiRange TranslateAndCreateBuffer(MemoryManager memoryManager, ulong gpuVa, ulong size)
+ public MultiRange TranslateAndCreateBuffer(MemoryManager memoryManager, ulong gpuVa, ulong size, BufferStage stage)
{
if (gpuVa == 0)
{
@@ -119,7 +120,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (address != MemoryManager.PteUnmapped)
{
- CreateBuffer(address, size);
+ CreateBuffer(address, size, stage);
}
return new MultiRange(address, size);
@@ -132,8 +133,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// GPU memory manager where the buffer is mapped
/// Start GPU virtual address of the buffer
/// Size in bytes of the buffer
+ /// The type of usage that created the buffer
/// Physical ranges of the buffer, after address translation
- public MultiRange TranslateAndCreateMultiBuffers(MemoryManager memoryManager, ulong gpuVa, ulong size)
+ public MultiRange TranslateAndCreateMultiBuffers(MemoryManager memoryManager, ulong gpuVa, ulong size, BufferStage stage)
{
if (gpuVa == 0)
{
@@ -149,7 +151,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
return range;
}
- CreateBuffer(range);
+ CreateBuffer(range, stage);
return range;
}
@@ -161,8 +163,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// GPU memory manager where the buffer is mapped
/// Start GPU virtual address of the buffer
/// Size in bytes of the buffer
+ /// The type of usage that created the buffer
/// Physical ranges of the buffer, after address translation
- public MultiRange TranslateAndCreateMultiBuffersPhysicalOnly(MemoryManager memoryManager, ulong gpuVa, ulong size)
+ public MultiRange TranslateAndCreateMultiBuffersPhysicalOnly(MemoryManager memoryManager, ulong gpuVa, ulong size, BufferStage stage)
{
if (gpuVa == 0)
{
@@ -186,11 +189,11 @@ namespace Ryujinx.Graphics.Gpu.Memory
{
if (range.Count > 1)
{
- CreateBuffer(subRange.Address, subRange.Size, SparseBufferAlignmentSize);
+ CreateBuffer(subRange.Address, subRange.Size, stage, SparseBufferAlignmentSize);
}
else
{
- CreateBuffer(subRange.Address, subRange.Size);
+ CreateBuffer(subRange.Address, subRange.Size, stage);
}
}
}
@@ -203,11 +206,12 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// This can be used to ensure the existance of a buffer.
///
/// Physical ranges of memory where the buffer data is located
- public void CreateBuffer(MultiRange range)
+ /// The type of usage that created the buffer
+ public void CreateBuffer(MultiRange range, BufferStage stage)
{
if (range.Count > 1)
{
- CreateMultiRangeBuffer(range);
+ CreateMultiRangeBuffer(range, stage);
}
else
{
@@ -215,7 +219,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (subRange.Address != MemoryManager.PteUnmapped)
{
- CreateBuffer(subRange.Address, subRange.Size);
+ CreateBuffer(subRange.Address, subRange.Size, stage);
}
}
}
@@ -226,7 +230,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
///
/// Address of the buffer in memory
/// Size of the buffer in bytes
- public void CreateBuffer(ulong address, ulong size)
+ /// The type of usage that created the buffer
+ public void CreateBuffer(ulong address, ulong size, BufferStage stage)
{
ulong endAddress = address + size;
@@ -239,7 +244,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
alignedEndAddress += BufferAlignmentSize;
}
- CreateBufferAligned(alignedAddress, alignedEndAddress - alignedAddress);
+ CreateBufferAligned(alignedAddress, alignedEndAddress - alignedAddress, stage);
}
///
@@ -248,8 +253,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
///
/// Address of the buffer in memory
/// Size of the buffer in bytes
+ /// The type of usage that created the buffer
/// Alignment of the start address of the buffer in bytes
- public void CreateBuffer(ulong address, ulong size, ulong alignment)
+ public void CreateBuffer(ulong address, ulong size, BufferStage stage, ulong alignment)
{
ulong alignmentMask = alignment - 1;
ulong pageAlignmentMask = BufferAlignmentMask;
@@ -264,7 +270,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
alignedEndAddress += pageAlignmentMask;
}
- CreateBufferAligned(alignedAddress, alignedEndAddress - alignedAddress, alignment);
+ CreateBufferAligned(alignedAddress, alignedEndAddress - alignedAddress, stage, alignment);
}
///
@@ -272,7 +278,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// if it does not exist yet.
///
/// Physical ranges of memory
- private void CreateMultiRangeBuffer(MultiRange range)
+ /// The type of usage that created the buffer
+ private void CreateMultiRangeBuffer(MultiRange range, BufferStage stage)
{
// Ensure all non-contiguous buffer we might use are sparse aligned.
for (int i = 0; i < range.Count; i++)
@@ -281,7 +288,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (subRange.Address != MemoryManager.PteUnmapped)
{
- CreateBuffer(subRange.Address, subRange.Size, SparseBufferAlignmentSize);
+ CreateBuffer(subRange.Address, subRange.Size, stage, SparseBufferAlignmentSize);
}
}
@@ -431,9 +438,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
result.EndGpuAddress < gpuVa + size ||
result.UnmappedSequence != result.Buffer.UnmappedSequence)
{
- MultiRange range = TranslateAndCreateBuffer(memoryManager, gpuVa, size);
+ MultiRange range = TranslateAndCreateBuffer(memoryManager, gpuVa, size, BufferStage.Internal);
ulong address = range.GetSubRange(0).Address;
- result = new BufferCacheEntry(address, gpuVa, GetBuffer(address, size));
+ result = new BufferCacheEntry(address, gpuVa, GetBuffer(address, size, BufferStage.Internal));
_dirtyCache[gpuVa] = result;
}
@@ -466,9 +473,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
result.EndGpuAddress < alignedEndGpuVa ||
result.UnmappedSequence != result.Buffer.UnmappedSequence)
{
- MultiRange range = TranslateAndCreateBuffer(memoryManager, alignedGpuVa, size);
+ MultiRange range = TranslateAndCreateBuffer(memoryManager, alignedGpuVa, size, BufferStage.None);
ulong address = range.GetSubRange(0).Address;
- result = new BufferCacheEntry(address, alignedGpuVa, GetBuffer(address, size));
+ result = new BufferCacheEntry(address, alignedGpuVa, GetBuffer(address, size, BufferStage.None));
_modifiedCache[alignedGpuVa] = result;
}
@@ -485,7 +492,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
///
/// Address of the buffer in guest memory
/// Size in bytes of the buffer
- private void CreateBufferAligned(ulong address, ulong size)
+ /// The type of usage that created the buffer
+ private void CreateBufferAligned(ulong address, ulong size, BufferStage stage)
{
Buffer[] overlaps = _bufferOverlaps;
int overlapsCount = _buffers.FindOverlapsNonOverlapping(address, size, ref overlaps);
@@ -546,13 +554,13 @@ namespace Ryujinx.Graphics.Gpu.Memory
ulong newSize = endAddress - address;
- CreateBufferAligned(address, newSize, anySparseCompatible, overlaps, overlapsCount);
+ CreateBufferAligned(address, newSize, stage, anySparseCompatible, overlaps, overlapsCount);
}
}
else
{
// No overlap, just create a new buffer.
- Buffer buffer = new(_context, _physicalMemory, address, size, sparseCompatible: false);
+ Buffer buffer = new(_context, _physicalMemory, address, size, stage, sparseCompatible: false);
lock (_buffers)
{
@@ -570,8 +578,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
///
/// Address of the buffer in guest memory
/// Size in bytes of the buffer
+ /// The type of usage that created the buffer
/// Alignment of the start address of the buffer
- private void CreateBufferAligned(ulong address, ulong size, ulong alignment)
+ private void CreateBufferAligned(ulong address, ulong size, BufferStage stage, ulong alignment)
{
Buffer[] overlaps = _bufferOverlaps;
int overlapsCount = _buffers.FindOverlapsNonOverlapping(address, size, ref overlaps);
@@ -624,13 +633,13 @@ namespace Ryujinx.Graphics.Gpu.Memory
ulong newSize = endAddress - address;
- CreateBufferAligned(address, newSize, sparseAligned, overlaps, overlapsCount);
+ CreateBufferAligned(address, newSize, stage, sparseAligned, overlaps, overlapsCount);
}
}
else
{
// No overlap, just create a new buffer.
- Buffer buffer = new(_context, _physicalMemory, address, size, sparseAligned);
+ Buffer buffer = new(_context, _physicalMemory, address, size, stage, sparseAligned);
lock (_buffers)
{
@@ -648,12 +657,13 @@ namespace Ryujinx.Graphics.Gpu.Memory
///
/// Address of the buffer in guest memory
/// Size in bytes of the buffer
+ /// The type of usage that created the buffer
/// Indicates if the buffer can be used in a sparse buffer mapping
/// Buffers overlapping the range
/// Total of overlaps
- private void CreateBufferAligned(ulong address, ulong size, bool sparseCompatible, Buffer[] overlaps, int overlapsCount)
+ private void CreateBufferAligned(ulong address, ulong size, BufferStage stage, bool sparseCompatible, Buffer[] overlaps, int overlapsCount)
{
- Buffer newBuffer = new Buffer(_context, _physicalMemory, address, size, sparseCompatible, overlaps.Take(overlapsCount));
+ Buffer newBuffer = new Buffer(_context, _physicalMemory, address, size, stage, sparseCompatible, overlaps.Take(overlapsCount));
lock (_buffers)
{
@@ -704,7 +714,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
for (int index = 0; index < overlapCount; index++)
{
- CreateMultiRangeBuffer(overlaps[index].Range);
+ CreateMultiRangeBuffer(overlaps[index].Range, BufferStage.None);
}
}
@@ -731,8 +741,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Size in bytes of the copy
public void CopyBuffer(MemoryManager memoryManager, ulong srcVa, ulong dstVa, ulong size)
{
- MultiRange srcRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, srcVa, size);
- MultiRange dstRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, dstVa, size);
+ MultiRange srcRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, srcVa, size, BufferStage.Copy);
+ MultiRange dstRange = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, dstVa, size, BufferStage.Copy);
if (srcRange.Count == 1 && dstRange.Count == 1)
{
@@ -788,8 +798,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Size in bytes of the copy
private void CopyBufferSingleRange(MemoryManager memoryManager, ulong srcAddress, ulong dstAddress, ulong size)
{
- Buffer srcBuffer = GetBuffer(srcAddress, size);
- Buffer dstBuffer = GetBuffer(dstAddress, size);
+ Buffer srcBuffer = GetBuffer(srcAddress, size, BufferStage.Copy);
+ Buffer dstBuffer = GetBuffer(dstAddress, size, BufferStage.Copy);
int srcOffset = (int)(srcAddress - srcBuffer.Address);
int dstOffset = (int)(dstAddress - dstBuffer.Address);
@@ -803,7 +813,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (srcBuffer.IsModified(srcAddress, size))
{
- dstBuffer.SignalModified(dstAddress, size);
+ dstBuffer.SignalModified(dstAddress, size, BufferStage.Copy);
}
else
{
@@ -828,12 +838,12 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Value to be written into the buffer
public void ClearBuffer(MemoryManager memoryManager, ulong gpuVa, ulong size, uint value)
{
- MultiRange range = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, gpuVa, size);
+ MultiRange range = TranslateAndCreateMultiBuffersPhysicalOnly(memoryManager, gpuVa, size, BufferStage.Copy);
for (int index = 0; index < range.Count; index++)
{
MemoryRange subRange = range.GetSubRange(index);
- Buffer buffer = GetBuffer(subRange.Address, subRange.Size);
+ Buffer buffer = GetBuffer(subRange.Address, subRange.Size, BufferStage.Copy);
int offset = (int)(subRange.Address - buffer.Address);
@@ -849,18 +859,19 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Gets a buffer sub-range starting at a given memory address, aligned to the next page boundary.
///
/// Physical regions of memory where the buffer is mapped
+ /// Buffer stage that triggered the access
/// Whether the buffer will be written to by this use
/// The buffer sub-range starting at the given memory address
- public BufferRange GetBufferRangeAligned(MultiRange range, bool write = false)
+ public BufferRange GetBufferRangeAligned(MultiRange range, BufferStage stage, bool write = false)
{
if (range.Count > 1)
{
- return GetBuffer(range, write).GetRange(range);
+ return GetBuffer(range, stage, write).GetRange(range);
}
else
{
MemoryRange subRange = range.GetSubRange(0);
- return GetBuffer(subRange.Address, subRange.Size, write).GetRangeAligned(subRange.Address, subRange.Size, write);
+ return GetBuffer(subRange.Address, subRange.Size, stage, write).GetRangeAligned(subRange.Address, subRange.Size, write);
}
}
@@ -868,18 +879,19 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Gets a buffer sub-range for a given memory range.
///
/// Physical regions of memory where the buffer is mapped
+ /// Buffer stage that triggered the access
/// Whether the buffer will be written to by this use
/// The buffer sub-range for the given range
- public BufferRange GetBufferRange(MultiRange range, bool write = false)
+ public BufferRange GetBufferRange(MultiRange range, BufferStage stage, bool write = false)
{
if (range.Count > 1)
{
- return GetBuffer(range, write).GetRange(range);
+ return GetBuffer(range, stage, write).GetRange(range);
}
else
{
MemoryRange subRange = range.GetSubRange(0);
- return GetBuffer(subRange.Address, subRange.Size, write).GetRange(subRange.Address, subRange.Size, write);
+ return GetBuffer(subRange.Address, subRange.Size, stage, write).GetRange(subRange.Address, subRange.Size, write);
}
}
@@ -888,9 +900,10 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// A buffer overlapping with the specified range is assumed to already exist on the cache.
///
/// Physical regions of memory where the buffer is mapped
+ /// Buffer stage that triggered the access
/// Whether the buffer will be written to by this use
/// The buffer where the range is fully contained
- private MultiRangeBuffer GetBuffer(MultiRange range, bool write = false)
+ private MultiRangeBuffer GetBuffer(MultiRange range, BufferStage stage, bool write = false)
{
for (int i = 0; i < range.Count; i++)
{
@@ -902,7 +915,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (write)
{
- subBuffer.SignalModified(subRange.Address, subRange.Size);
+ subBuffer.SignalModified(subRange.Address, subRange.Size, stage);
}
}
@@ -935,9 +948,10 @@ namespace Ryujinx.Graphics.Gpu.Memory
///
/// Start address of the memory range
/// Size in bytes of the memory range
+ /// Buffer stage that triggered the access
/// Whether the buffer will be written to by this use
/// The buffer where the range is fully contained
- private Buffer GetBuffer(ulong address, ulong size, bool write = false)
+ private Buffer GetBuffer(ulong address, ulong size, BufferStage stage, bool write = false)
{
Buffer buffer;
@@ -950,7 +964,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (write)
{
- buffer.SignalModified(address, size);
+ buffer.SignalModified(address, size, stage);
}
}
else
@@ -1004,6 +1018,18 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
}
+ ///
+ /// Signal that the given buffer's handle has changed,
+ /// forcing rebind and any overlapping multi-range buffers to be recreated.
+ ///
+ /// The buffer that has changed handle
+ public void BufferBackingChanged(Buffer buffer)
+ {
+ NotifyBuffersModified?.Invoke();
+
+ RecreateMultiRangeBuffers(buffer.Address, buffer.Size);
+ }
+
///
/// Prune any invalid entries from a quick access dictionary.
///
diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs
index 8f2201e0a..26d9501c6 100644
--- a/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs
+++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs
@@ -156,7 +156,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Type of each index buffer element
public void SetIndexBuffer(ulong gpuVa, ulong size, IndexType type)
{
- MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size);
+ MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size, BufferStage.IndexBuffer);
_indexBuffer.Range = range;
_indexBuffer.Type = type;
@@ -186,7 +186,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Vertex divisor of the buffer, for instanced draws
public void SetVertexBuffer(int index, ulong gpuVa, ulong size, int stride, int divisor)
{
- MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size);
+ MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size, BufferStage.VertexBuffer);
_vertexBuffers[index].Range = range;
_vertexBuffers[index].Stride = stride;
@@ -213,7 +213,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Size in bytes of the transform feedback buffer
public void SetTransformFeedbackBuffer(int index, ulong gpuVa, ulong size)
{
- MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size);
+ MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStage.TransformFeedback);
_transformFeedbackBuffers[index] = new BufferBounds(range);
_transformFeedbackBuffersDirty = true;
@@ -260,7 +260,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
gpuVa = BitUtils.AlignDown(gpuVa, (ulong)_context.Capabilities.StorageBufferOffsetAlignment);
- MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size);
+ MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStageUtils.ComputeStorage(flags));
_cpStorageBuffers.SetBounds(index, range, flags);
}
@@ -284,7 +284,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
gpuVa = BitUtils.AlignDown(gpuVa, (ulong)_context.Capabilities.StorageBufferOffsetAlignment);
- MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size);
+ MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStageUtils.GraphicsStorage(stage, flags));
if (!buffers.Buffers[index].Range.Equals(range))
{
@@ -303,7 +303,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Size in bytes of the storage buffer
public void SetComputeUniformBuffer(int index, ulong gpuVa, ulong size)
{
- MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size);
+ MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size, BufferStage.Compute);
_cpUniformBuffers.SetBounds(index, range);
}
@@ -318,7 +318,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Size in bytes of the storage buffer
public void SetGraphicsUniformBuffer(int stage, int index, ulong gpuVa, ulong size)
{
- MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size);
+ MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateBuffer(_channel.MemoryManager, gpuVa, size, BufferStageUtils.FromShaderStage(stage));
_gpUniformBuffers[stage].SetBounds(index, range);
_gpUniformBuffersDirty = true;
@@ -502,7 +502,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
foreach (var binding in _bufferTextures)
{
var isStore = binding.BindingInfo.Flags.HasFlag(TextureUsageFlags.ImageStore);
- var range = bufferCache.GetBufferRange(binding.Range, isStore);
+ var range = bufferCache.GetBufferRange(binding.Range, BufferStageUtils.TextureBuffer(binding.Stage, binding.BindingInfo.Flags), isStore);
binding.Texture.SetStorage(range);
// The texture must be rebound to use the new storage if it was updated.
@@ -526,7 +526,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
foreach (var binding in _bufferTextureArrays)
{
- var range = bufferCache.GetBufferRange(binding.Range);
+ var range = bufferCache.GetBufferRange(binding.Range, BufferStage.None);
binding.Texture.SetStorage(range);
textureArray[0] = binding.Texture;
@@ -536,7 +536,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
foreach (var binding in _bufferImageArrays)
{
var isStore = binding.BindingInfo.Flags.HasFlag(TextureUsageFlags.ImageStore);
- var range = bufferCache.GetBufferRange(binding.Range, isStore);
+ var range = bufferCache.GetBufferRange(binding.Range, BufferStage.None, isStore);
binding.Texture.SetStorage(range);
textureArray[0] = binding.Texture;
@@ -565,7 +565,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (!_indexBuffer.Range.IsUnmapped)
{
- BufferRange buffer = bufferCache.GetBufferRange(_indexBuffer.Range);
+ BufferRange buffer = bufferCache.GetBufferRange(_indexBuffer.Range, BufferStage.IndexBuffer);
_context.Renderer.Pipeline.SetIndexBuffer(buffer, _indexBuffer.Type);
}
@@ -597,7 +597,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
continue;
}
- BufferRange buffer = bufferCache.GetBufferRange(vb.Range);
+ BufferRange buffer = bufferCache.GetBufferRange(vb.Range, BufferStage.VertexBuffer);
vertexBuffers[index] = new VertexBufferDescriptor(buffer, vb.Stride, vb.Divisor);
}
@@ -637,7 +637,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
continue;
}
- tfbs[index] = bufferCache.GetBufferRange(tfb.Range, write: true);
+ tfbs[index] = bufferCache.GetBufferRange(tfb.Range, BufferStage.TransformFeedback, write: true);
}
_context.Renderer.Pipeline.SetTransformFeedbackBuffers(tfbs);
@@ -684,7 +684,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
_context.SupportBufferUpdater.SetTfeOffset(index, tfeOffset);
- buffers[index] = new BufferAssignment(index, bufferCache.GetBufferRange(range, write: true));
+ buffers[index] = new BufferAssignment(index, bufferCache.GetBufferRange(range, BufferStage.TransformFeedback, write: true));
}
}
@@ -751,6 +751,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
for (ShaderStage stage = ShaderStage.Vertex; stage <= ShaderStage.Fragment; stage++)
{
ref var buffers = ref bindings[(int)stage - 1];
+ BufferStage bufferStage = BufferStageUtils.FromShaderStage(stage);
for (int index = 0; index < buffers.Count; index++)
{
@@ -762,8 +763,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
{
var isWrite = bounds.Flags.HasFlag(BufferUsageFlags.Write);
var range = isStorage
- ? bufferCache.GetBufferRangeAligned(bounds.Range, isWrite)
- : bufferCache.GetBufferRange(bounds.Range);
+ ? bufferCache.GetBufferRangeAligned(bounds.Range, bufferStage | BufferStageUtils.FromUsage(bounds.Flags), isWrite)
+ : bufferCache.GetBufferRange(bounds.Range, bufferStage);
ranges[rangesCount++] = new BufferAssignment(bindingInfo.Binding, range);
}
@@ -799,8 +800,8 @@ namespace Ryujinx.Graphics.Gpu.Memory
{
var isWrite = bounds.Flags.HasFlag(BufferUsageFlags.Write);
var range = isStorage
- ? bufferCache.GetBufferRangeAligned(bounds.Range, isWrite)
- : bufferCache.GetBufferRange(bounds.Range);
+ ? bufferCache.GetBufferRangeAligned(bounds.Range, BufferStageUtils.ComputeStorage(bounds.Flags), isWrite)
+ : bufferCache.GetBufferRange(bounds.Range, BufferStage.Compute);
ranges[rangesCount++] = new BufferAssignment(bindingInfo.Binding, range);
}
@@ -875,7 +876,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
Format format,
bool isImage)
{
- _channel.MemoryManager.Physical.BufferCache.CreateBuffer(range);
+ _channel.MemoryManager.Physical.BufferCache.CreateBuffer(range, BufferStageUtils.TextureBuffer(stage, bindingInfo.Flags));
_bufferTextures.Add(new BufferTextureBinding(stage, texture, range, bindingInfo, format, isImage));
}
@@ -883,6 +884,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
///
/// Sets the buffer storage of a buffer texture array element. This will be bound when the buffer manager commits bindings.
///
+ /// Shader stage accessing the texture
/// Texture array where the element will be inserted
/// Buffer texture
/// Physical ranges of memory where the buffer texture data is located
@@ -890,6 +892,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Index of the binding on the array
/// Format of the buffer texture
public void SetBufferTextureStorage(
+ ShaderStage stage,
ITextureArray array,
ITexture texture,
MultiRange range,
@@ -897,7 +900,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
int index,
Format format)
{
- _channel.MemoryManager.Physical.BufferCache.CreateBuffer(range);
+ _channel.MemoryManager.Physical.BufferCache.CreateBuffer(range, BufferStageUtils.TextureBuffer(stage, bindingInfo.Flags));
_bufferTextureArrays.Add(new BufferTextureArrayBinding(array, texture, range, bindingInfo, index, format));
}
@@ -905,6 +908,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
///
/// Sets the buffer storage of a buffer image array element. This will be bound when the buffer manager commits bindings.
///
+ /// Shader stage accessing the texture
/// Image array where the element will be inserted
/// Buffer texture
/// Physical ranges of memory where the buffer texture data is located
@@ -912,6 +916,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Index of the binding on the array
/// Format of the buffer texture
public void SetBufferTextureStorage(
+ ShaderStage stage,
IImageArray array,
ITexture texture,
MultiRange range,
@@ -919,7 +924,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
int index,
Format format)
{
- _channel.MemoryManager.Physical.BufferCache.CreateBuffer(range);
+ _channel.MemoryManager.Physical.BufferCache.CreateBuffer(range, BufferStageUtils.TextureBuffer(stage, bindingInfo.Flags));
_bufferImageArrays.Add(new BufferTextureArrayBinding(array, texture, range, bindingInfo, index, format));
}
diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferMigration.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferMigration.cs
index 0a5268031..ce9985318 100644
--- a/src/Ryujinx.Graphics.Gpu/Memory/BufferMigration.cs
+++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferMigration.cs
@@ -1,37 +1,21 @@
using System;
+using System.Threading;
namespace Ryujinx.Graphics.Gpu.Memory
{
///
- /// A record of when buffer data was copied from one buffer to another, along with the SyncNumber when the migration will be complete.
- /// Keeps the source buffer alive for data flushes until the migration is complete.
+ /// A record of when buffer data was copied from multiple buffers to one migration target,
+ /// along with the SyncNumber when the migration will be complete.
+ /// Keeps the source buffers alive for data flushes until the migration is complete.
+ /// All spans cover the full range of the "destination" buffer.
///
internal class BufferMigration : IDisposable
{
///
- /// The offset for the migrated region.
+ /// Ranges from source buffers that were copied as part of this migration.
+ /// Ordered by increasing base address.
///
- private readonly ulong _offset;
-
- ///
- /// The size for the migrated region.
- ///
- private readonly ulong _size;
-
- ///
- /// The buffer that was migrated from.
- ///
- private readonly Buffer _buffer;
-
- ///
- /// The source range action, to be called on overlap with an unreached sync number.
- ///
- private readonly Action _sourceRangeAction;
-
- ///
- /// The source range list.
- ///
- private readonly BufferModifiedRangeList _source;
+ public BufferMigrationSpan[] Spans { get; private set; }
///
/// The destination range list. This range list must be updated when flushing the source.
@@ -43,55 +27,193 @@ namespace Ryujinx.Graphics.Gpu.Memory
///
public readonly ulong SyncNumber;
+ ///
+ /// Number of active users there are traversing this migration's spans.
+ ///
+ private int _refCount;
+
+ ///
+ /// Create a new buffer migration.
+ ///
+ /// Source spans for the migration
+ /// Destination buffer range list
+ /// Sync number where this migration will be complete
+ public BufferMigration(BufferMigrationSpan[] spans, BufferModifiedRangeList destination, ulong syncNumber)
+ {
+ Spans = spans;
+ Destination = destination;
+ SyncNumber = syncNumber;
+ }
+
+ ///
+ /// Add a span to the migration. Allocates a new array with the target size, and replaces it.
+ ///
+ ///
+ /// The base address for the span is assumed to be higher than all other spans in the migration,
+ /// to keep the span array ordered.
+ ///
+ public void AddSpanToEnd(BufferMigrationSpan span)
+ {
+ BufferMigrationSpan[] oldSpans = Spans;
+
+ BufferMigrationSpan[] newSpans = new BufferMigrationSpan[oldSpans.Length + 1];
+
+ oldSpans.CopyTo(newSpans, 0);
+
+ newSpans[oldSpans.Length] = span;
+
+ Spans = newSpans;
+ }
+
+ ///
+ /// Performs the given range action, or one from a migration that overlaps and has not synced yet.
+ ///
+ /// The offset to pass to the action
+ /// The size to pass to the action
+ /// The sync number that has been reached
+ /// The action to perform
+ public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber, BufferFlushAction rangeAction)
+ {
+ long syncDiff = (long)(syncNumber - SyncNumber);
+
+ if (syncDiff >= 0)
+ {
+ // The migration has completed. Run the parent action.
+ rangeAction(offset, size, syncNumber);
+ }
+ else
+ {
+ Interlocked.Increment(ref _refCount);
+
+ ulong prevAddress = offset;
+ ulong endAddress = offset + size;
+
+ foreach (BufferMigrationSpan span in Spans)
+ {
+ if (!span.Overlaps(offset, size))
+ {
+ continue;
+ }
+
+ if (span.Address > prevAddress)
+ {
+ // There's a gap between this span and the last (or the start address). Flush the range using the parent action.
+
+ rangeAction(prevAddress, span.Address - prevAddress, syncNumber);
+ }
+
+ span.RangeActionWithMigration(offset, size, syncNumber);
+
+ prevAddress = span.Address + span.Size;
+ }
+
+ if (endAddress > prevAddress)
+ {
+ // There's a gap at the end of the range with no migration. Flush the range using the parent action.
+ rangeAction(prevAddress, endAddress - prevAddress, syncNumber);
+ }
+
+ Interlocked.Decrement(ref _refCount);
+ }
+ }
+
+ ///
+ /// Dispose the buffer migration. This removes the reference from the destination range list,
+ /// and runs all the dispose buffers for the migration spans. (typically disposes the source buffer)
+ ///
+ public void Dispose()
+ {
+ while (Volatile.Read(ref _refCount) > 0)
+ {
+ // Coming into this method, the sync for the migration will be met, so nothing can increment the ref count.
+ // However, an existing traversal of the spans for data flush could still be in progress.
+ // Spin if this is ever the case, so they don't get disposed before the operation is complete.
+ }
+
+ Destination.RemoveMigration(this);
+
+ foreach (BufferMigrationSpan span in Spans)
+ {
+ span.Dispose();
+ }
+ }
+ }
+
+ ///
+ /// A record of when buffer data was copied from one buffer to another, for a specific range in a source buffer.
+ /// Keeps the source buffer alive for data flushes until the migration is complete.
+ ///
+ internal readonly struct BufferMigrationSpan : IDisposable
+ {
+ ///
+ /// The offset for the migrated region.
+ ///
+ public readonly ulong Address;
+
+ ///
+ /// The size for the migrated region.
+ ///
+ public readonly ulong Size;
+
+ ///
+ /// The action to perform when the migration isn't needed anymore.
+ ///
+ private readonly Action _disposeAction;
+
+ ///
+ /// The source range action, to be called on overlap with an unreached sync number.
+ ///
+ private readonly BufferFlushAction _sourceRangeAction;
+
+ ///
+ /// Optional migration for the source data. Can chain together if many migrations happen in a short time.
+ /// If this is null, then _sourceRangeAction will always provide up to date data.
+ ///
+ private readonly BufferMigration _source;
+
///
/// Creates a record for a buffer migration.
///
/// The source buffer for this migration
+ /// The action to perform when the migration isn't needed anymore
/// The flush action for the source buffer
- /// The modified range list for the source buffer
- /// The modified range list for the destination buffer
- /// The sync number for when the migration is complete
- public BufferMigration(
+ /// Pending migration for the source buffer
+ public BufferMigrationSpan(
Buffer buffer,
- Action sourceRangeAction,
- BufferModifiedRangeList source,
- BufferModifiedRangeList dest,
- ulong syncNumber)
+ Action disposeAction,
+ BufferFlushAction sourceRangeAction,
+ BufferMigration source)
{
- _offset = buffer.Address;
- _size = buffer.Size;
- _buffer = buffer;
+ Address = buffer.Address;
+ Size = buffer.Size;
+ _disposeAction = disposeAction;
_sourceRangeAction = sourceRangeAction;
_source = source;
- Destination = dest;
- SyncNumber = syncNumber;
}
+ ///
+ /// Creates a record for a buffer migration, using the default buffer dispose action.
+ ///
+ /// The source buffer for this migration
+ /// The flush action for the source buffer
+ /// Pending migration for the source buffer
+ public BufferMigrationSpan(
+ Buffer buffer,
+ BufferFlushAction sourceRangeAction,
+ BufferMigration source) : this(buffer, buffer.DecrementReferenceCount, sourceRangeAction, source) { }
+
///
/// Determine if the given range overlaps this migration, and has not been completed yet.
///
/// Start offset
/// Range size
- /// The sync number that was waited on
/// True if overlapping and in progress, false otherwise
- public bool Overlaps(ulong offset, ulong size, ulong syncNumber)
+ public bool Overlaps(ulong offset, ulong size)
{
ulong end = offset + size;
- ulong destEnd = _offset + _size;
- long syncDiff = (long)(syncNumber - SyncNumber); // syncNumber is less if the copy has not completed.
+ ulong destEnd = Address + Size;
- return !(end <= _offset || offset >= destEnd) && syncDiff < 0;
- }
-
- ///
- /// Determine if the given range matches this migration.
- ///
- /// Start offset
- /// Range size
- /// True if the range exactly matches, false otherwise
- public bool FullyMatches(ulong offset, ulong size)
- {
- return _offset == offset && _size == size;
+ return !(end <= Address || offset >= destEnd);
}
///
@@ -100,26 +222,30 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// Start offset
/// Range size
/// Current sync number
- /// The modified range list that originally owned this range
- public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber, BufferModifiedRangeList parent)
+ public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber)
{
ulong end = offset + size;
- end = Math.Min(_offset + _size, end);
- offset = Math.Max(_offset, offset);
+ end = Math.Min(Address + Size, end);
+ offset = Math.Max(Address, offset);
size = end - offset;
- _source.RangeActionWithMigration(offset, size, syncNumber, parent, _sourceRangeAction);
+ if (_source != null)
+ {
+ _source.RangeActionWithMigration(offset, size, syncNumber, _sourceRangeAction);
+ }
+ else
+ {
+ _sourceRangeAction(offset, size, syncNumber);
+ }
}
///
- /// Removes this reference to the range list, potentially allowing for the source buffer to be disposed.
+ /// Removes this migration span, potentially allowing for the source buffer to be disposed.
///
public void Dispose()
{
- Destination.RemoveMigration(this);
-
- _buffer.DecrementReferenceCount();
+ _disposeAction();
}
}
}
diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs
index 6ada8a4b2..d330de638 100644
--- a/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs
+++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs
@@ -1,7 +1,6 @@
using Ryujinx.Common.Pools;
using Ryujinx.Memory.Range;
using System;
-using System.Collections.Generic;
using System.Linq;
namespace Ryujinx.Graphics.Gpu.Memory
@@ -72,10 +71,10 @@ namespace Ryujinx.Graphics.Gpu.Memory
private readonly GpuContext _context;
private readonly Buffer _parent;
- private readonly Action _flushAction;
+ private readonly BufferFlushAction _flushAction;
- private List _sources;
- private BufferMigration _migrationTarget;
+ private BufferMigration _source;
+ private BufferModifiedRangeList _migrationTarget;
private readonly object _lock = new();
@@ -99,7 +98,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// GPU context that the buffer range list belongs to
/// The parent buffer that owns this range list
/// The flush action for the parent buffer
- public BufferModifiedRangeList(GpuContext context, Buffer parent, Action flushAction) : base(BackingInitialSize)
+ public BufferModifiedRangeList(GpuContext context, Buffer parent, BufferFlushAction flushAction) : base(BackingInitialSize)
{
_context = context;
_parent = parent;
@@ -199,6 +198,36 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
}
+ ///
+ /// Gets modified ranges within the specified region, and then fires the given action for each range individually.
+ ///
+ /// Start address to query
+ /// Size to query
+ /// Sync number required for a range to be signalled
+ /// The action to call for each modified range
+ public void GetRangesAtSync(ulong address, ulong size, ulong syncNumber, Action rangeAction)
+ {
+ int count = 0;
+
+ ref var overlaps = ref ThreadStaticArray.Get();
+
+ // Range list must be consistent for this operation.
+ lock (_lock)
+ {
+ count = FindOverlapsNonOverlapping(address, size, ref overlaps);
+ }
+
+ for (int i = 0; i < count; i++)
+ {
+ BufferModifiedRange overlap = overlaps[i];
+
+ if (overlap.SyncNumber == syncNumber)
+ {
+ rangeAction(overlap.Address, overlap.Size);
+ }
+ }
+ }
+
///
/// Gets modified ranges within the specified region, and then fires the given action for each range individually.
///
@@ -245,41 +274,16 @@ namespace Ryujinx.Graphics.Gpu.Memory
/// The offset to pass to the action
/// The size to pass to the action
/// The sync number that has been reached
- /// The modified range list that originally owned this range
/// The action to perform
- public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber, BufferModifiedRangeList parent, Action rangeAction)
+ public void RangeActionWithMigration(ulong offset, ulong size, ulong syncNumber, BufferFlushAction rangeAction)
{
- bool firstSource = true;
-
- if (parent != this)
+ if (_source != null)
{
- lock (_lock)
- {
- if (_sources != null)
- {
- foreach (BufferMigration source in _sources)
- {
- if (source.Overlaps(offset, size, syncNumber))
- {
- if (firstSource && !source.FullyMatches(offset, size))
- {
- // Perform this buffer's action first. The migrations will run after.
- rangeAction(offset, size);
- }
-
- source.RangeActionWithMigration(offset, size, syncNumber, parent);
-
- firstSource = false;
- }
- }
- }
- }
+ _source.RangeActionWithMigration(offset, size, syncNumber, rangeAction);
}
-
- if (firstSource)
+ else
{
- // No overlapping migrations, or they are not meant for this range, flush the data using the given action.
- rangeAction(offset, size);
+ rangeAction(offset, size, syncNumber);
}
}
@@ -319,7 +323,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
ClearPart(overlap, clampAddress, clampEnd);
- RangeActionWithMigration(clampAddress, clampEnd - clampAddress, waitSync, overlap.Parent, _flushAction);
+ RangeActionWithMigration(clampAddress, clampEnd - clampAddress, waitSync, _flushAction);
}
}
@@ -329,7 +333,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
// There is a migration target to call instead. This can't be changed after set so accessing it outside the lock is fine.
- _migrationTarget.Destination.RemoveRangesAndFlush(overlaps, rangeCount, highestDiff, currentSync, address, endAddress);
+ _migrationTarget.RemoveRangesAndFlush(overlaps, rangeCount, highestDiff, currentSync, address, endAddress);
}
///
@@ -367,7 +371,7 @@ namespace Ryujinx.Graphics.Gpu.Memory
if (rangeCount == -1)
{
- _migrationTarget.Destination.WaitForAndFlushRanges(address, size);
+ _migrationTarget.WaitForAndFlushRanges(address, size);
return;
}
@@ -407,6 +411,9 @@ namespace Ryujinx.Graphics.Gpu.Memory
///
/// Inherit ranges from another modified range list.
///
+ ///
+ /// Assumes that ranges will be inherited in address ascending order.
+ ///
/// The range list to inherit from
/// The action to call for each modified range
public void InheritRanges(BufferModifiedRangeList ranges, Action registerRangeAction)
@@ -415,18 +422,31 @@ namespace Ryujinx.Graphics.Gpu.Memory
lock (ranges._lock)
{
- BufferMigration migration = new(ranges._parent, ranges._flushAction, ranges, this, _context.SyncNumber);
-
- ranges._parent.IncrementReferenceCount();
- ranges._migrationTarget = migration;
-
- _context.RegisterBufferMigration(migration);
-
inheritRanges = ranges.ToArray();
lock (_lock)
{
- (_sources ??= new List()).Add(migration);
+ // Copy over the migration from the previous range list
+
+ BufferMigration oldMigration = ranges._source;
+
+ BufferMigrationSpan span = new BufferMigrationSpan(ranges._parent, ranges._flushAction, oldMigration);
+ ranges._parent.IncrementReferenceCount();
+
+ if (_source == null)
+ {
+ // Create a new migration.
+ _source = new BufferMigration(new BufferMigrationSpan[] { span }, this, _context.SyncNumber);
+
+ _context.RegisterBufferMigration(_source);
+ }
+ else
+ {
+ // Extend the migration
+ _source.AddSpanToEnd(span);
+ }
+
+ ranges._migrationTarget = this;
foreach (BufferModifiedRange range in inheritRanges)
{
@@ -445,6 +465,27 @@ namespace Ryujinx.Graphics.Gpu.Memory
}
}
+ ///
+ /// Register a migration from previous buffer storage. This migration is from a snapshot of the buffer's
+ /// current handle to its handle in the future, and is assumed to be complete when the sync action completes.
+ /// When the migration completes, the handle is disposed.
+ ///
+ public void SelfMigration()
+ {
+ lock (_lock)
+ {
+ BufferMigrationSpan span = new(_parent, _parent.GetSnapshotDisposeAction(), _parent.GetSnapshotFlushAction(), _source);
+ BufferMigration migration = new(new BufferMigrationSpan[] { span }, this, _context.SyncNumber);
+
+ // Migration target is used to redirect flush actions to the latest range list,
+ // so we don't need to set it here. (this range list is still the latest)
+
+ _context.RegisterBufferMigration(migration);
+
+ _source = migration;
+ }
+ }
+
///
/// Removes a source buffer migration, indicating its copy has completed.
///
@@ -453,7 +494,10 @@ namespace Ryujinx.Graphics.Gpu.Memory
{
lock (_lock)
{
- _sources.Remove(migration);
+ if (_source == migration)
+ {
+ _source = null;
+ }
}
}
diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferPreFlush.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferPreFlush.cs
new file mode 100644
index 000000000..d58b9ea66
--- /dev/null
+++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferPreFlush.cs
@@ -0,0 +1,295 @@
+using Ryujinx.Common;
+using Ryujinx.Graphics.GAL;
+using System;
+
+namespace Ryujinx.Graphics.Gpu.Memory
+{
+ ///
+ /// Manages flushing ranges from buffers in advance for easy access, if they are flushed often.
+ /// Typically, from device local memory to a host mapped target for cached access.
+ ///
+ internal class BufferPreFlush : IDisposable
+ {
+ private const ulong PageSize = MemoryManager.PageSize;
+
+ ///
+ /// Threshold for the number of copies without a flush required to disable preflush on a page.
+ ///
+ private const int DeactivateCopyThreshold = 200;
+
+ ///
+ /// Value that indicates whether a page has been flushed or copied before.
+ ///
+ private enum PreFlushState
+ {
+ None,
+ HasFlushed,
+ HasCopied
+ }
+
+ ///
+ /// Flush state for each page of the buffer.
+ /// Controls whether data should be copied to the flush buffer, what sync is expected
+ /// and unflushed copy counting for stopping copies that are no longer needed.
+ ///
+ private struct PreFlushPage
+ {
+ public PreFlushState State;
+ public ulong FirstActivatedSync;
+ public ulong LastCopiedSync;
+ public int CopyCount;
+ }
+
+ ///
+ /// True if there are ranges that should copy to the flush buffer, false otherwise.
+ ///
+ public bool ShouldCopy { get; private set; }
+
+ private readonly GpuContext _context;
+ private readonly Buffer _buffer;
+ private readonly PreFlushPage[] _pages;
+ private readonly ulong _address;
+ private readonly ulong _size;
+ private readonly ulong _misalignment;
+ private readonly Action _flushAction;
+
+ private BufferHandle _flushBuffer;
+
+ public BufferPreFlush(GpuContext context, Buffer parent, Action flushAction)
+ {
+ _context = context;
+ _buffer = parent;
+ _address = parent.Address;
+ _size = parent.Size;
+ _pages = new PreFlushPage[BitUtils.DivRoundUp(_size, PageSize)];
+ _misalignment = _address & (PageSize - 1);
+
+ _flushAction = flushAction;
+ }
+
+ ///
+ /// Ensure that the flush buffer exists.
+ ///
+ private void EnsureFlushBuffer()
+ {
+ if (_flushBuffer == BufferHandle.Null)
+ {
+ _flushBuffer = _context.Renderer.CreateBuffer((int)_size, BufferAccess.HostMemory);
+ }
+ }
+
+ ///
+ /// Gets a page range from an address and size byte range.
+ ///
+ /// Range address
+ /// Range size
+ /// A page index and count
+ private (int index, int count) GetPageRange(ulong address, ulong size)
+ {
+ ulong offset = address - _address;
+ ulong endOffset = offset + size;
+
+ int basePage = (int)(offset / PageSize);
+ int endPage = (int)((endOffset - 1) / PageSize);
+
+ return (basePage, 1 + endPage - basePage);
+ }
+
+ ///
+ /// Gets an offset and size range in the parent buffer from a page index and count.
+ ///
+ /// Range start page
+ /// Range page count
+ /// Offset and size range
+ private (int offset, int size) GetOffset(int startPage, int count)
+ {
+ int offset = (int)((ulong)startPage * PageSize - _misalignment);
+ int endOffset = (int)((ulong)(startPage + count) * PageSize - _misalignment);
+
+ offset = Math.Max(0, offset);
+ endOffset = Math.Min((int)_size, endOffset);
+
+ return (offset, endOffset - offset);
+ }
+
+ ///
+ /// Copy a range of pages from the parent buffer into the flush buffer.
+ ///
+ /// Range start page
+ /// Range page count
+ private void CopyPageRange(int startPage, int count)
+ {
+ (int offset, int size) = GetOffset(startPage, count);
+
+ EnsureFlushBuffer();
+
+ _context.Renderer.Pipeline.CopyBuffer(_buffer.Handle, _flushBuffer, offset, offset, size);
+ }
+
+ ///
+ /// Copy a modified range into the flush buffer if it's marked as flushed.
+ /// Any pages the range overlaps are copied, and copies aren't repeated in the same sync number.
+ ///
+ /// Range address
+ /// Range size
+ public void CopyModified(ulong address, ulong size)
+ {
+ (int baseIndex, int count) = GetPageRange(address, size);
+ ulong syncNumber = _context.SyncNumber;
+
+ int startPage = -1;
+
+ for (int i = 0; i < count; i++)
+ {
+ int pageIndex = baseIndex + i;
+ ref PreFlushPage page = ref _pages[pageIndex];
+
+ if (page.State > PreFlushState.None)
+ {
+ // Perform the copy, and update the state of each page.
+ if (startPage == -1)
+ {
+ startPage = pageIndex;
+ }
+
+ if (page.State != PreFlushState.HasCopied)
+ {
+ page.FirstActivatedSync = syncNumber;
+ page.State = PreFlushState.HasCopied;
+ }
+ else if (page.CopyCount++ >= DeactivateCopyThreshold)
+ {
+ page.CopyCount = 0;
+ page.State = PreFlushState.None;
+ }
+
+ if (page.LastCopiedSync != syncNumber)
+ {
+ page.LastCopiedSync = syncNumber;
+ }
+ }
+ else if (startPage != -1)
+ {
+ CopyPageRange(startPage, pageIndex - startPage);
+
+ startPage = -1;
+ }
+ }
+
+ if (startPage != -1)
+ {
+ CopyPageRange(startPage, (baseIndex + count) - startPage);
+ }
+ }
+
+ ///
+ /// Flush the given page range back into guest memory, optionally using data from the flush buffer.
+ /// The actual flushed range is an intersection of the page range and the address range.
+ ///
+ /// Address range start
+ /// Address range size
+ /// Page range start
+ /// Page range count
+ /// True if the data should come from the flush buffer
+ private void FlushPageRange(ulong address, ulong size, int startPage, int count, bool preFlush)
+ {
+ (int pageOffset, int pageSize) = GetOffset(startPage, count);
+
+ int offset = (int)(address - _address);
+ int end = offset + (int)size;
+
+ offset = Math.Max(offset, pageOffset);
+ end = Math.Min(end, pageOffset + pageSize);
+
+ if (end >= offset)
+ {
+ BufferHandle handle = preFlush ? _flushBuffer : _buffer.Handle;
+ _flushAction(handle, _address + (ulong)offset, (ulong)(end - offset));
+ }
+ }
+
+ ///
+ /// Flush the given address range back into guest memory, optionally using data from the flush buffer.
+ /// When a copy has been performed on or before the waited sync number, the data can come from the flush buffer.
+ /// Otherwise, it flushes the parent buffer directly.
+ ///
+ /// Range address
+ /// Range size
+ /// Sync number that has been waited for
+ public void FlushWithAction(ulong address, ulong size, ulong syncNumber)
+ {
+ // Copy the parts of the range that have pre-flush copies that have been completed.
+ // Run the flush action for ranges that don't have pre-flush copies.
+
+ // If a range doesn't have a pre-flush copy, consider adding one.
+
+ (int baseIndex, int count) = GetPageRange(address, size);
+
+ bool rangePreFlushed = false;
+ int startPage = -1;
+
+ for (int i = 0; i < count; i++)
+ {
+ int pageIndex = baseIndex + i;
+ ref PreFlushPage page = ref _pages[pageIndex];
+
+ bool flushPage = false;
+ page.CopyCount = 0;
+
+ if (page.State == PreFlushState.HasCopied)
+ {
+ if (syncNumber >= page.FirstActivatedSync)
+ {
+ // After the range is first activated, its data will always be copied to the preflush buffer on each sync.
+ flushPage = true;
+ }
+ }
+ else if (page.State == PreFlushState.None)
+ {
+ page.State = PreFlushState.HasFlushed;
+ ShouldCopy = true;
+ }
+
+ if (flushPage)
+ {
+ if (!rangePreFlushed || startPage == -1)
+ {
+ if (startPage != -1)
+ {
+ FlushPageRange(address, size, startPage, pageIndex - startPage, false);
+ }
+
+ rangePreFlushed = true;
+ startPage = pageIndex;
+ }
+ }
+ else if (rangePreFlushed || startPage == -1)
+ {
+ if (startPage != -1)
+ {
+ FlushPageRange(address, size, startPage, pageIndex - startPage, true);
+ }
+
+ rangePreFlushed = false;
+ startPage = pageIndex;
+ }
+ }
+
+ if (startPage != -1)
+ {
+ FlushPageRange(address, size, startPage, (baseIndex + count) - startPage, rangePreFlushed);
+ }
+ }
+
+ ///
+ /// Dispose the flush buffer, if present.
+ ///
+ public void Dispose()
+ {
+ if (_flushBuffer != BufferHandle.Null)
+ {
+ _context.Renderer.DeleteBuffer(_flushBuffer);
+ }
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferStage.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferStage.cs
new file mode 100644
index 000000000..d56abda28
--- /dev/null
+++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferStage.cs
@@ -0,0 +1,99 @@
+using Ryujinx.Graphics.Shader;
+using System.Runtime.CompilerServices;
+
+namespace Ryujinx.Graphics.Gpu.Memory
+{
+ ///
+ /// Pipeline stages that can modify buffer data, as well as flags indicating storage usage.
+ /// Must match ShaderStage for the shader stages, though anything after that can be in any order.
+ ///
+ internal enum BufferStage : byte
+ {
+ Compute,
+ Vertex,
+ TessellationControl,
+ TessellationEvaluation,
+ Geometry,
+ Fragment,
+
+ Indirect,
+ VertexBuffer,
+ IndexBuffer,
+ Copy,
+ TransformFeedback,
+ Internal,
+ None,
+
+ StageMask = 0x3f,
+ StorageMask = 0xc0,
+
+ StorageRead = 0x40,
+ StorageWrite = 0x80,
+
+#pragma warning disable CA1069 // Enums values should not be duplicated
+ StorageAtomic = 0xc0
+#pragma warning restore CA1069 // Enums values should not be duplicated
+ }
+
+ ///
+ /// Utility methods to convert shader stages and binding flags into buffer stages.
+ ///
+ internal static class BufferStageUtils
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static BufferStage FromShaderStage(ShaderStage stage)
+ {
+ return (BufferStage)stage;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static BufferStage FromShaderStage(int stageIndex)
+ {
+ return (BufferStage)(stageIndex + 1);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static BufferStage FromUsage(BufferUsageFlags flags)
+ {
+ if (flags.HasFlag(BufferUsageFlags.Write))
+ {
+ return BufferStage.StorageWrite;
+ }
+ else
+ {
+ return BufferStage.StorageRead;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static BufferStage FromUsage(TextureUsageFlags flags)
+ {
+ if (flags.HasFlag(TextureUsageFlags.ImageStore))
+ {
+ return BufferStage.StorageWrite;
+ }
+ else
+ {
+ return BufferStage.StorageRead;
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static BufferStage TextureBuffer(ShaderStage shaderStage, TextureUsageFlags flags)
+ {
+ return FromShaderStage(shaderStage) | FromUsage(flags);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static BufferStage GraphicsStorage(int stageIndex, BufferUsageFlags flags)
+ {
+ return FromShaderStage(stageIndex) | FromUsage(flags);
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static BufferStage ComputeStorage(BufferUsageFlags flags)
+ {
+ return BufferStage.Compute | FromUsage(flags);
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
index 2a39ae446..7bcff947e 100644
--- a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
+++ b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
@@ -61,7 +61,9 @@ namespace Ryujinx.Graphics.OpenGL
{
BufferCount++;
- if (access.HasFlag(GAL.BufferAccess.FlushPersistent))
+ var memType = access & GAL.BufferAccess.MemoryTypeMask;
+
+ if (memType == GAL.BufferAccess.HostMemory)
{
BufferHandle handle = Buffer.CreatePersistent(size);
@@ -75,11 +77,6 @@ namespace Ryujinx.Graphics.OpenGL
}
}
- public BufferHandle CreateBuffer(int size, GAL.BufferAccess access, BufferHandle storageHint)
- {
- return CreateBuffer(size, access);
- }
-
public BufferHandle CreateBuffer(nint pointer, int size)
{
throw new NotSupportedException();
@@ -148,6 +145,7 @@ namespace Ryujinx.Graphics.OpenGL
return new Capabilities(
api: TargetApi.OpenGL,
vendorName: GpuVendor,
+ memoryType: SystemMemoryType.BackendManaged,
hasFrontFacingBug: intelWindows,
hasVectorIndexingBug: amdWindows,
needsFragmentOutputSpecialization: false,
diff --git a/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs b/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs
index 3673ee5a1..3dcbc3130 100644
--- a/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs
+++ b/src/Ryujinx.Graphics.Vulkan/BufferHolder.cs
@@ -1,4 +1,3 @@
-using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Silk.NET.Vulkan;
using System;
@@ -31,40 +30,29 @@ namespace Ryujinx.Graphics.Vulkan
private readonly VulkanRenderer _gd;
private readonly Device _device;
- private MemoryAllocation _allocation;
- private Auto _buffer;
- private Auto _allocationAuto;
+ private readonly MemoryAllocation _allocation;
+ private readonly Auto _buffer;
+ private readonly Auto _allocationAuto;
private readonly bool _allocationImported;
- private ulong _bufferHandle;
+ private readonly ulong _bufferHandle;
private CacheByRange _cachedConvertedBuffers;
public int Size { get; }
- private IntPtr _map;
+ private readonly IntPtr _map;
- private MultiFenceHolder _waitable;
+ private readonly MultiFenceHolder _waitable;
private bool _lastAccessIsWrite;
- private BufferAllocationType _baseType;
- private BufferAllocationType _currentType;
- private bool _swapQueued;
-
- public BufferAllocationType DesiredType { get; private set; }
-
- private int _setCount;
- private int _writeCount;
- private int _flushCount;
- private int _flushTemp;
- private int _lastFlushWrite = -1;
+ private readonly BufferAllocationType _baseType;
+ private readonly BufferAllocationType _activeType;
private readonly ReaderWriterLockSlim _flushLock;
private FenceHolder _flushFence;
private int _flushWaiting;
- private List _swapActions;
-
private byte[] _pendingData;
private BufferMirrorRangeList _pendingDataRanges;
private Dictionary _mirrors;
@@ -83,8 +71,7 @@ namespace Ryujinx.Graphics.Vulkan
_map = allocation.HostPointer;
_baseType = type;
- _currentType = currentType;
- DesiredType = currentType;
+ _activeType = currentType;
_flushLock = new ReaderWriterLockSlim();
_useMirrors = gd.IsTBDR;
@@ -104,8 +91,7 @@ namespace Ryujinx.Graphics.Vulkan
_map = _allocation.HostPointer + offset;
_baseType = type;
- _currentType = currentType;
- DesiredType = currentType;
+ _activeType = currentType;
_flushLock = new ReaderWriterLockSlim();
}
@@ -120,164 +106,11 @@ namespace Ryujinx.Graphics.Vulkan
Size = size;
_baseType = BufferAllocationType.Sparse;
- _currentType = BufferAllocationType.Sparse;
- DesiredType = BufferAllocationType.Sparse;
+ _activeType = BufferAllocationType.Sparse;
_flushLock = new ReaderWriterLockSlim();
}
- public bool TryBackingSwap(ref CommandBufferScoped? cbs)
- {
- if (_swapQueued && DesiredType != _currentType)
- {
- // Only swap if the buffer is not used in any queued command buffer.
- bool isRented = _buffer.HasRentedCommandBufferDependency(_gd.CommandBufferPool);
-
- if (!isRented && _gd.CommandBufferPool.OwnedByCurrentThread && !_flushLock.IsReadLockHeld && (_pendingData == null || cbs != null))
- {
- var currentAllocation = _allocationAuto;
- var currentBuffer = _buffer;
- IntPtr currentMap = _map;
-
- (VkBuffer buffer, MemoryAllocation allocation, BufferAllocationType resultType) = _gd.BufferManager.CreateBacking(_gd, Size, DesiredType, false, false, _currentType);
-
- if (buffer.Handle != 0)
- {
- if (cbs != null)
- {
- ClearMirrors(cbs.Value, 0, Size);
- }
-
- _flushLock.EnterWriteLock();
-
- ClearFlushFence();
-
- _waitable = new MultiFenceHolder(Size);
-
- _allocation = allocation;
- _allocationAuto = new Auto(allocation);
- _buffer = new Auto(new DisposableBuffer(_gd.Api, _device, buffer), this, _waitable, _allocationAuto);
- _bufferHandle = buffer.Handle;
- _map = allocation.HostPointer;
-
- if (_map != IntPtr.Zero && currentMap != IntPtr.Zero)
- {
- // Copy data directly. Readbacks don't have to wait if this is done.
-
- unsafe
- {
- new Span((void*)currentMap, Size).CopyTo(new Span((void*)_map, Size));
- }
- }
- else
- {
- cbs ??= _gd.CommandBufferPool.Rent();
-
- CommandBufferScoped cbsV = cbs.Value;
-
- Copy(_gd, cbsV, currentBuffer, _buffer, 0, 0, Size);
-
- // Need to wait for the data to reach the new buffer before data can be flushed.
-
- _flushFence = _gd.CommandBufferPool.GetFence(cbsV.CommandBufferIndex);
- _flushFence.Get();
- }
-
- Logger.Debug?.PrintMsg(LogClass.Gpu, $"Converted {Size} buffer {_currentType} to {resultType}");
-
- _currentType = resultType;
-
- if (_swapActions != null)
- {
- foreach (var action in _swapActions)
- {
- action();
- }
-
- _swapActions.Clear();
- }
-
- currentBuffer.Dispose();
- currentAllocation.Dispose();
-
- _gd.PipelineInternal.SwapBuffer(currentBuffer, _buffer);
-
- _flushLock.ExitWriteLock();
- }
-
- _swapQueued = false;
-
- return true;
- }
-
- return false;
- }
-
- _swapQueued = false;
-
- return true;
- }
-
- private void ConsiderBackingSwap()
- {
- if (_baseType == BufferAllocationType.Auto)
- {
- // When flushed, wait for a bit more info to make a decision.
- bool wasFlushed = _flushTemp > 0;
- int multiplier = wasFlushed ? 2 : 0;
- if (_writeCount >= (WriteCountThreshold << multiplier) || _setCount >= (SetCountThreshold << multiplier) || _flushCount >= (FlushCountThreshold << multiplier))
- {
- if (_flushCount > 0 || _flushTemp-- > 0)
- {
- // Buffers that flush should ideally be mapped in host address space for easy copies.
- // If the buffer is large it will do better on GPU memory, as there will be more writes than data flushes (typically individual pages).
- // If it is small, then it's likely most of the buffer will be flushed so we want it on host memory, as access is cached.
-
- bool hostMappingSensitive = _gd.Vendor == Vendor.Nvidia;
- bool deviceLocalMapped = Size > DeviceLocalSizeThreshold || (wasFlushed && _writeCount > _flushCount * 10 && hostMappingSensitive) || _currentType == BufferAllocationType.DeviceLocalMapped;
-
- DesiredType = deviceLocalMapped ? BufferAllocationType.DeviceLocalMapped : BufferAllocationType.HostMapped;
-
- // It's harder for a buffer that is flushed to revert to another type of mapping.
- if (_flushCount > 0)
- {
- _flushTemp = 1000;
- }
- }
- else if (_writeCount >= (WriteCountThreshold << multiplier))
- {
- // Buffers that are written often should ideally be in the device local heap. (Storage buffers)
- DesiredType = BufferAllocationType.DeviceLocal;
- }
- else if (_setCount > (SetCountThreshold << multiplier))
- {
- // Buffers that have their data set often should ideally be host mapped. (Constant buffers)
- DesiredType = BufferAllocationType.HostMapped;
- }
-
- _lastFlushWrite = -1;
- _flushCount = 0;
- _writeCount = 0;
- _setCount = 0;
- }
-
- if (!_swapQueued && DesiredType != _currentType)
- {
- _swapQueued = true;
-
- _gd.PipelineInternal.AddBackingSwap(this);
- }
- }
- }
-
- public void Pin()
- {
- if (_baseType == BufferAllocationType.Auto)
- {
- _baseType = _currentType;
- }
- }
-
public unsafe Auto CreateView(VkFormat format, int offset, int size, Action invalidateView)
{
var bufferViewCreateInfo = new BufferViewCreateInfo
@@ -291,19 +124,9 @@ namespace Ryujinx.Graphics.Vulkan
_gd.Api.CreateBufferView(_device, bufferViewCreateInfo, null, out var bufferView).ThrowOnError();
- (_swapActions ??= new List()).Add(invalidateView);
-
return new Auto(new DisposableBufferView(_gd.Api, _device, bufferView), this, _waitable, _buffer);
}
- public void InheritMetrics(BufferHolder other)
- {
- _setCount = other._setCount;
- _writeCount = other._writeCount;
- _flushCount = other._flushCount;
- _flushTemp = other._flushTemp;
- }
-
public unsafe void InsertBarrier(CommandBuffer commandBuffer, bool isWrite)
{
// If the last access is write, we always need a barrier to be sure we will read or modify
@@ -423,18 +246,8 @@ namespace Ryujinx.Graphics.Vulkan
{
if (isWrite)
{
- _writeCount++;
-
SignalWrite(0, Size);
}
- else if (isSSBO)
- {
- // Always consider SSBO access for swapping to device local memory.
-
- _writeCount++;
-
- ConsiderBackingSwap();
- }
return _buffer;
}
@@ -443,8 +256,6 @@ namespace Ryujinx.Graphics.Vulkan
{
if (isWrite)
{
- _writeCount++;
-
SignalWrite(offset, size);
}
@@ -543,8 +354,6 @@ namespace Ryujinx.Graphics.Vulkan
public void SignalWrite(int offset, int size)
{
- ConsiderBackingSwap();
-
if (offset == 0 && size == Size)
{
_cachedConvertedBuffers.Clear();
@@ -624,13 +433,6 @@ namespace Ryujinx.Graphics.Vulkan
WaitForFlushFence();
- if (_lastFlushWrite != _writeCount)
- {
- // If it's on the same page as the last flush, ignore it.
- _lastFlushWrite = _writeCount;
- _flushCount++;
- }
-
Span result;
if (_map != IntPtr.Zero)
@@ -711,8 +513,7 @@ namespace Ryujinx.Graphics.Vulkan
return;
}
- _setCount++;
- bool allowMirror = _useMirrors && allowCbsWait && cbs != null && _currentType <= BufferAllocationType.HostMapped;
+ bool allowMirror = _useMirrors && allowCbsWait && cbs != null && _activeType <= BufferAllocationType.HostMapped;
if (_map != IntPtr.Zero)
{
@@ -863,8 +664,6 @@ namespace Ryujinx.Graphics.Vulkan
var dstBuffer = GetBuffer(cbs.CommandBuffer, dstOffset, data.Length, true).Get(cbs, dstOffset, data.Length, true).Value;
- _writeCount--;
-
InsertBufferBarrier(
_gd,
cbs.CommandBuffer,
@@ -1100,8 +899,6 @@ namespace Ryujinx.Graphics.Vulkan
public void Dispose()
{
- _swapQueued = false;
-
_gd.PipelineInternal?.FlushCommandsIfWeightExceeding(_buffer, (ulong)Size);
_buffer.Dispose();
diff --git a/src/Ryujinx.Graphics.Vulkan/BufferManager.cs b/src/Ryujinx.Graphics.Vulkan/BufferManager.cs
index 33289a0e0..e73cde83c 100644
--- a/src/Ryujinx.Graphics.Vulkan/BufferManager.cs
+++ b/src/Ryujinx.Graphics.Vulkan/BufferManager.cs
@@ -165,10 +165,6 @@ namespace Ryujinx.Graphics.Vulkan
if (TryGetBuffer(range.Handle, out var existingHolder))
{
- // Since this buffer now also owns the memory from the referenced buffer,
- // we pin it to ensure the memory location will not change.
- existingHolder.Pin();
-
(var memory, var offset) = existingHolder.GetDeviceMemoryAndOffset();
memoryBinds[index] = new SparseMemoryBind()
@@ -235,10 +231,9 @@ namespace Ryujinx.Graphics.Vulkan
int size,
bool sparseCompatible = false,
BufferAllocationType baseType = BufferAllocationType.HostMapped,
- BufferHandle storageHint = default,
bool forceMirrors = false)
{
- return CreateWithHandle(gd, size, out _, sparseCompatible, baseType, storageHint, forceMirrors);
+ return CreateWithHandle(gd, size, out _, sparseCompatible, baseType, forceMirrors);
}
public BufferHandle CreateWithHandle(
@@ -247,10 +242,9 @@ namespace Ryujinx.Graphics.Vulkan
out BufferHolder holder,
bool sparseCompatible = false,
BufferAllocationType baseType = BufferAllocationType.HostMapped,
- BufferHandle storageHint = default,
bool forceMirrors = false)
{
- holder = Create(gd, size, forConditionalRendering: false, sparseCompatible, baseType, storageHint);
+ holder = Create(gd, size, forConditionalRendering: false, sparseCompatible, baseType);
if (holder == null)
{
return BufferHandle.Null;
@@ -387,31 +381,13 @@ namespace Ryujinx.Graphics.Vulkan
int size,
bool forConditionalRendering = false,
bool sparseCompatible = false,
- BufferAllocationType baseType = BufferAllocationType.HostMapped,
- BufferHandle storageHint = default)
+ BufferAllocationType baseType = BufferAllocationType.HostMapped)
{
BufferAllocationType type = baseType;
- BufferHolder storageHintHolder = null;
if (baseType == BufferAllocationType.Auto)
{
- if (gd.IsSharedMemory)
- {
- baseType = BufferAllocationType.HostMapped;
- type = baseType;
- }
- else
- {
- type = size >= BufferHolder.DeviceLocalSizeThreshold ? BufferAllocationType.DeviceLocal : BufferAllocationType.HostMapped;
- }
-
- if (storageHint != BufferHandle.Null)
- {
- if (TryGetBuffer(storageHint, out storageHintHolder))
- {
- type = storageHintHolder.DesiredType;
- }
- }
+ type = BufferAllocationType.HostMapped;
}
(VkBuffer buffer, MemoryAllocation allocation, BufferAllocationType resultType) =
@@ -421,11 +397,6 @@ namespace Ryujinx.Graphics.Vulkan
{
var holder = new BufferHolder(gd, _device, buffer, allocation, size, baseType, resultType);
- if (storageHintHolder != null)
- {
- holder.InheritMetrics(storageHintHolder);
- }
-
return holder;
}
diff --git a/src/Ryujinx.Graphics.Vulkan/EnumConversion.cs b/src/Ryujinx.Graphics.Vulkan/EnumConversion.cs
index f9243bf83..9d1fd9ffd 100644
--- a/src/Ryujinx.Graphics.Vulkan/EnumConversion.cs
+++ b/src/Ryujinx.Graphics.Vulkan/EnumConversion.cs
@@ -424,10 +424,20 @@ namespace Ryujinx.Graphics.Vulkan
public static BufferAllocationType Convert(this BufferAccess access)
{
- if (access.HasFlag(BufferAccess.FlushPersistent) || access.HasFlag(BufferAccess.Stream))
+ BufferAccess memType = access & BufferAccess.MemoryTypeMask;
+
+ if (memType == BufferAccess.HostMemory || access.HasFlag(BufferAccess.Stream))
{
return BufferAllocationType.HostMapped;
}
+ else if (memType == BufferAccess.DeviceMemory)
+ {
+ return BufferAllocationType.DeviceLocal;
+ }
+ else if (memType == BufferAccess.DeviceMemoryMapped)
+ {
+ return BufferAllocationType.DeviceLocalMapped;
+ }
return BufferAllocationType.Auto;
}
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs b/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs
index 4987548cd..357d517eb 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs
@@ -222,20 +222,6 @@ namespace Ryujinx.Graphics.Vulkan
}
}
- private void TryBackingSwaps()
- {
- CommandBufferScoped? cbs = null;
-
- _backingSwaps.RemoveAll(holder => holder.TryBackingSwap(ref cbs));
-
- cbs?.Dispose();
- }
-
- public void AddBackingSwap(BufferHolder holder)
- {
- _backingSwaps.Add(holder);
- }
-
public void Restore()
{
if (Pipeline != null)
@@ -291,8 +277,6 @@ namespace Ryujinx.Graphics.Vulkan
Gd.ResetCounterPool();
- TryBackingSwaps();
-
Restore();
}
diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
index 8ef05de36..175d5e3ea 100644
--- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
+++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
@@ -486,12 +486,7 @@ namespace Ryujinx.Graphics.Vulkan
public BufferHandle CreateBuffer(int size, BufferAccess access)
{
- return BufferManager.CreateWithHandle(this, size, access.HasFlag(BufferAccess.SparseCompatible), access.Convert(), default, access == BufferAccess.Stream);
- }
-
- public BufferHandle CreateBuffer(int size, BufferAccess access, BufferHandle storageHint)
- {
- return BufferManager.CreateWithHandle(this, size, access.HasFlag(BufferAccess.SparseCompatible), access.Convert(), storageHint);
+ return BufferManager.CreateWithHandle(this, size, access.HasFlag(BufferAccess.SparseCompatible), access.Convert(), access.HasFlag(BufferAccess.Stream));
}
public BufferHandle CreateBuffer(nint pointer, int size)
@@ -675,9 +670,23 @@ namespace Ryujinx.Graphics.Vulkan
var limits = _physicalDevice.PhysicalDeviceProperties.Limits;
var mainQueueProperties = _physicalDevice.QueueFamilyProperties[QueueFamilyIndex];
+ SystemMemoryType memoryType;
+
+ if (IsSharedMemory)
+ {
+ memoryType = SystemMemoryType.UnifiedMemory;
+ }
+ else
+ {
+ memoryType = Vendor == Vendor.Nvidia ?
+ SystemMemoryType.DedicatedMemorySlowStorage :
+ SystemMemoryType.DedicatedMemory;
+ }
+
return new Capabilities(
api: TargetApi.Vulkan,
GpuVendor,
+ memoryType: memoryType,
hasFrontFacingBug: IsIntelWindows,
hasVectorIndexingBug: Vendor == Vendor.Qualcomm,
needsFragmentOutputSpecialization: IsMoltenVk,
From c634eb4054c2e7f530307198d7cc6a20b3666d7d Mon Sep 17 00:00:00 2001
From: Logan Stromberg
Date: Mon, 20 May 2024 14:38:38 -0700
Subject: [PATCH 025/109] Updating Concentus dependency to speed up Opus
decoding (#6757)
* Implementing new features in the latest Concentus library - span-in, span-out Opus decoding (so we don't have to make temporary buffer copies), returning a more precise error code from the decoder, and automatically linking the native opus library with P/invoke if supported on the current system
* Remove stub log messages and commit package upgrade to 2.1.0
* use more correct disposal pattern
* Bump to Concentus 2.1.1
* Bump to Concentus 2.1.2
* Don't bother pulling in native opus binaries from Concentus package (using ExcludeAssets).
* Fix opus MS channel count. Explicitly disable native lib probe in OpusCodecFactory.
* Bump to package 2.2.0 which has split out the native libs, as suggested.
---------
Co-authored-by: Logan Stromberg
---
Directory.Packages.props | 4 +-
src/Ryujinx.Horizon/Ryujinx.Horizon.csproj | 8 +-
.../Sdk/Codec/Detail/HardwareOpusDecoder.cs | 91 +++++++++++++------
3 files changed, 68 insertions(+), 35 deletions(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index d04e237e0..739e66bd0 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -11,7 +11,7 @@
-
+
@@ -49,4 +49,4 @@
-
+
\ No newline at end of file
diff --git a/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj b/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj
index d1f572d5c..bf34ddd17 100644
--- a/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj
+++ b/src/Ryujinx.Horizon/Ryujinx.Horizon.csproj
@@ -1,4 +1,4 @@
-
+
net8.0
@@ -16,10 +16,4 @@
-
-
-
- NU1605
-
-
diff --git a/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoder.cs b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoder.cs
index 5d2798582..2146362df 100644
--- a/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoder.cs
+++ b/src/Ryujinx.Horizon/Sdk/Codec/Detail/HardwareOpusDecoder.cs
@@ -14,6 +14,11 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail
{
partial class HardwareOpusDecoder : IHardwareOpusDecoder, IDisposable
{
+ static HardwareOpusDecoder()
+ {
+ OpusCodecFactory.AttemptToUseNativeLibrary = false;
+ }
+
[StructLayout(LayoutKind.Sequential)]
private struct OpusPacketHeader
{
@@ -30,60 +35,87 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail
}
}
- private interface IDecoder
+ private interface IDecoder : IDisposable
{
int SampleRate { get; }
int ChannelsCount { get; }
- int Decode(byte[] inData, int inDataOffset, int len, short[] outPcm, int outPcmOffset, int frameSize);
+ int Decode(ReadOnlySpan inData, Span outPcm, int frameSize);
void ResetState();
}
private class Decoder : IDecoder
{
- private readonly OpusDecoder _decoder;
+ private readonly IOpusDecoder _decoder;
public int SampleRate => _decoder.SampleRate;
public int ChannelsCount => _decoder.NumChannels;
public Decoder(int sampleRate, int channelsCount)
{
- _decoder = new OpusDecoder(sampleRate, channelsCount);
+ _decoder = OpusCodecFactory.CreateDecoder(sampleRate, channelsCount);
}
- public int Decode(byte[] inData, int inDataOffset, int len, short[] outPcm, int outPcmOffset, int frameSize)
+ public int Decode(ReadOnlySpan inData, Span outPcm, int frameSize)
{
- return _decoder.Decode(inData, inDataOffset, len, outPcm, outPcmOffset, frameSize);
+ return _decoder.Decode(inData, outPcm, frameSize);
}
public void ResetState()
{
_decoder.ResetState();
}
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _decoder?.Dispose();
+ }
+ }
}
private class MultiSampleDecoder : IDecoder
{
- private readonly OpusMSDecoder _decoder;
+ private readonly IOpusMultiStreamDecoder _decoder;
public int SampleRate => _decoder.SampleRate;
- public int ChannelsCount { get; }
+ public int ChannelsCount => _decoder.NumChannels;
public MultiSampleDecoder(int sampleRate, int channelsCount, int streams, int coupledStreams, byte[] mapping)
{
- ChannelsCount = channelsCount;
- _decoder = new OpusMSDecoder(sampleRate, channelsCount, streams, coupledStreams, mapping);
+ _decoder = OpusCodecFactory.CreateMultiStreamDecoder(sampleRate, channelsCount, streams, coupledStreams, mapping);
}
- public int Decode(byte[] inData, int inDataOffset, int len, short[] outPcm, int outPcmOffset, int frameSize)
+ public int Decode(ReadOnlySpan inData, Span outPcm, int frameSize)
{
- return _decoder.DecodeMultistream(inData, inDataOffset, len, outPcm, outPcmOffset, frameSize, 0);
+ return _decoder.DecodeMultistream(inData, outPcm, frameSize, false);
}
public void ResetState()
{
_decoder.ResetState();
}
+
+ public void Dispose()
+ {
+ Dispose(disposing: true);
+ GC.SuppressFinalize(this);
+ }
+
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _decoder?.Dispose();
+ }
+ }
}
private readonly IDecoder _decoder;
@@ -221,7 +253,8 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail
{
timeTaken = 0;
- Result result = DecodeInterleaved(_decoder, reset, input, out short[] outPcmData, output.Length, out outConsumed, out outSamples);
+ Span outPcmSpace = MemoryMarshal.Cast(output);
+ Result result = DecodeInterleaved(_decoder, reset, input, outPcmSpace, output.Length, out outConsumed, out outSamples);
if (withPerf)
{
@@ -229,14 +262,12 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail
timeTaken = 0;
}
- MemoryMarshal.Cast(outPcmData).CopyTo(output[..(outPcmData.Length * sizeof(short))]);
-
return result;
}
- private static Result GetPacketNumSamples(IDecoder decoder, out int numSamples, byte[] packet)
+ private static Result GetPacketNumSamples(IDecoder decoder, out int numSamples, ReadOnlySpan packet)
{
- int result = OpusPacketInfo.GetNumSamples(packet, 0, packet.Length, decoder.SampleRate);
+ int result = OpusPacketInfo.GetNumSamples(packet, decoder.SampleRate);
numSamples = result;
@@ -256,12 +287,11 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail
IDecoder decoder,
bool reset,
ReadOnlySpan input,
- out short[] outPcmData,
+ Span outPcmData,
int outputSize,
out int outConsumed,
out int outSamples)
{
- outPcmData = null;
outConsumed = 0;
outSamples = 0;
@@ -281,7 +311,7 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail
return CodecResult.InvalidLength;
}
- byte[] opusData = input.Slice(headerSize, (int)header.Length).ToArray();
+ ReadOnlySpan opusData = input.Slice(headerSize, (int)header.Length);
Result result = GetPacketNumSamples(decoder, out int numSamples, opusData);
@@ -292,8 +322,6 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail
return CodecResult.InvalidLength;
}
- outPcmData = new short[numSamples * decoder.ChannelsCount];
-
if (reset)
{
decoder.ResetState();
@@ -301,13 +329,22 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail
try
{
- outSamples = decoder.Decode(opusData, 0, opusData.Length, outPcmData, 0, outPcmData.Length / decoder.ChannelsCount);
+ outSamples = decoder.Decode(opusData, outPcmData, numSamples);
outConsumed = (int)totalSize;
}
- catch (OpusException)
+ catch (OpusException e)
{
- // TODO: As OpusException doesn't return the exact error code, this is inaccurate in some cases...
- return CodecResult.InvalidLength;
+ switch (e.OpusErrorCode)
+ {
+ case OpusError.OPUS_BUFFER_TOO_SMALL:
+ return CodecResult.InvalidLength;
+ case OpusError.OPUS_BAD_ARG:
+ return CodecResult.OpusBadArg;
+ case OpusError.OPUS_INVALID_PACKET:
+ return CodecResult.OpusInvalidPacket;
+ default:
+ return CodecResult.InvalidLength;
+ }
}
}
@@ -324,6 +361,8 @@ namespace Ryujinx.Horizon.Sdk.Codec.Detail
_workBufferHandle = 0;
}
+
+ _decoder?.Dispose();
}
}
From c1ed1509493cdf69b39fddc1ae8b2f3d877adec6 Mon Sep 17 00:00:00 2001
From: riperiperi
Date: Wed, 22 May 2024 21:47:27 +0100
Subject: [PATCH 026/109] Kernel: Wake cores from idle directly rather than
through a host thread (#6837)
* Kernel: Wake cores from idle directly rather than through a host thread
Right now when a core enters an idle state, leaving that idle state requires us to first signal the core's idle thread, which then signals the correct thread that we want to run on the core. This means that in a lot of cases, we're paying double for a thread to be woken from an idle state.
This PR moves this process to happen on the thread that is waking others out of idle, instead of an idle thread that needs to be woken first.
For compatibility the process has been kept as similar as possible - the process for IdleThreadLoop has been migrated to TryLeaveIdle, and is gated by a condition variable that lets it run only once at a time for each core. A core is only considered for wake from idle if idle is both active and has been signalled - the signal is consumed and the active state is cleared when the core leaves idle.
Dummy threads (just the idle thread at the moment) have been changed to have no host thread, as the work is now done by threads entering idle and signalling out of it.
This could put a bit of extra work on threads that would have triggered `_idleInterruptEvent` before, but I'd expect less work than signalling all those reset events and the OS overhead that follows. Worst case is that other threads performing these signals at the same time will have to wait for each other, but it's still going to be a very short amount of time.
Improvements are best seen in games with heavy (or very misguided) multithreading, such as Pokemon: Legends Arceus. Improvements are expected in Scarlet/Violet and TOTK, but are harder to measure.
Testing on Linux/MacOS still to be done, definitely need to test more games as this affects all of them (obviously) and any issues might be rare to encounter.
* Remove _idleThread entirely
* Use spinwait so we don't completely blast the CPU with cmpxchg
* Didn't I already do this
* Cleanup
---
.../HOS/Kernel/Threading/KScheduler.cs | 153 ++++++++++--------
.../HOS/Kernel/Threading/KThread.cs | 5 +-
.../HOS/Kernel/Threading/ThreadType.cs | 1 -
3 files changed, 91 insertions(+), 68 deletions(-)
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs
index 905c61d66..8ef77902c 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs
@@ -28,42 +28,25 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
private SchedulingState _state;
- private AutoResetEvent _idleInterruptEvent;
- private readonly object _idleInterruptEventLock;
-
private KThread _previousThread;
private KThread _currentThread;
- private readonly KThread _idleThread;
+
+ private int _coreIdleLock;
+ private bool _idleSignalled = true;
+ private bool _idleActive = true;
+ private long _idleTimeRunning;
public KThread PreviousThread => _previousThread;
public KThread CurrentThread => _currentThread;
public long LastContextSwitchTime { get; private set; }
- public long TotalIdleTimeTicks => _idleThread.TotalTimeRunning;
+ public long TotalIdleTimeTicks => _idleTimeRunning;
public KScheduler(KernelContext context, int coreId)
{
_context = context;
_coreId = coreId;
- _idleInterruptEvent = new AutoResetEvent(false);
- _idleInterruptEventLock = new object();
-
- KThread idleThread = CreateIdleThread(context, coreId);
-
- _currentThread = idleThread;
- _idleThread = idleThread;
-
- idleThread.StartHostThread();
- idleThread.SchedulerWaitEvent.Set();
- }
-
- private KThread CreateIdleThread(KernelContext context, int cpuCore)
- {
- KThread idleThread = new(context);
-
- idleThread.Initialize(0UL, 0UL, 0UL, PrioritiesCount, cpuCore, null, ThreadType.Dummy, IdleThreadLoop);
-
- return idleThread;
+ _currentThread = null;
}
public static ulong SelectThreads(KernelContext context)
@@ -237,39 +220,64 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
KThread threadToSignal = context.Schedulers[coreToSignal]._currentThread;
// Request the thread running on that core to stop and reschedule, if we have one.
- if (threadToSignal != context.Schedulers[coreToSignal]._idleThread)
- {
- threadToSignal.Context.RequestInterrupt();
- }
+ threadToSignal?.Context.RequestInterrupt();
// If the core is idle, ensure that the idle thread is awaken.
- context.Schedulers[coreToSignal]._idleInterruptEvent.Set();
+ context.Schedulers[coreToSignal].NotifyIdleThread();
scheduledCoresMask &= ~(1UL << coreToSignal);
}
}
- private void IdleThreadLoop()
+ private void ActivateIdleThread()
{
- while (_context.Running)
+ while (Interlocked.CompareExchange(ref _coreIdleLock, 1, 0) != 0)
+ {
+ Thread.SpinWait(1);
+ }
+
+ Thread.MemoryBarrier();
+
+ // Signals that idle thread is now active on this core.
+ _idleActive = true;
+
+ TryLeaveIdle();
+
+ Interlocked.Exchange(ref _coreIdleLock, 0);
+ }
+
+ private void NotifyIdleThread()
+ {
+ while (Interlocked.CompareExchange(ref _coreIdleLock, 1, 0) != 0)
+ {
+ Thread.SpinWait(1);
+ }
+
+ Thread.MemoryBarrier();
+
+ // Signals that the idle core may be able to exit idle.
+ _idleSignalled = true;
+
+ TryLeaveIdle();
+
+ Interlocked.Exchange(ref _coreIdleLock, 0);
+ }
+
+ public void TryLeaveIdle()
+ {
+ if (_idleSignalled && _idleActive)
{
_state.NeedsScheduling = false;
Thread.MemoryBarrier();
- KThread nextThread = PickNextThread(_state.SelectedThread);
+ KThread nextThread = PickNextThread(null, _state.SelectedThread);
- if (_idleThread != nextThread)
+ if (nextThread != null)
{
- _idleThread.SchedulerWaitEvent.Reset();
- WaitHandle.SignalAndWait(nextThread.SchedulerWaitEvent, _idleThread.SchedulerWaitEvent);
+ _idleActive = false;
+ nextThread.SchedulerWaitEvent.Set();
}
- _idleInterruptEvent.WaitOne();
- }
-
- lock (_idleInterruptEventLock)
- {
- _idleInterruptEvent.Dispose();
- _idleInterruptEvent = null;
+ _idleSignalled = false;
}
}
@@ -292,20 +300,37 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
// Wake all the threads that might be waiting until this thread context is unlocked.
for (int core = 0; core < CpuCoresCount; core++)
{
- _context.Schedulers[core]._idleInterruptEvent.Set();
+ _context.Schedulers[core].NotifyIdleThread();
}
- KThread nextThread = PickNextThread(selectedThread);
+ KThread nextThread = PickNextThread(KernelStatic.GetCurrentThread(), selectedThread);
if (currentThread.Context.Running)
{
// Wait until this thread is scheduled again, and allow the next thread to run.
- WaitHandle.SignalAndWait(nextThread.SchedulerWaitEvent, currentThread.SchedulerWaitEvent);
+
+ if (nextThread == null)
+ {
+ ActivateIdleThread();
+ currentThread.SchedulerWaitEvent.WaitOne();
+ }
+ else
+ {
+ WaitHandle.SignalAndWait(nextThread.SchedulerWaitEvent, currentThread.SchedulerWaitEvent);
+ }
}
else
{
// Allow the next thread to run.
- nextThread.SchedulerWaitEvent.Set();
+
+ if (nextThread == null)
+ {
+ ActivateIdleThread();
+ }
+ else
+ {
+ nextThread.SchedulerWaitEvent.Set();
+ }
// We don't need to wait since the thread is exiting, however we need to
// make sure this thread will never call the scheduler again, since it is
@@ -319,7 +344,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
}
}
- private KThread PickNextThread(KThread selectedThread)
+ private KThread PickNextThread(KThread currentThread, KThread selectedThread)
{
while (true)
{
@@ -335,7 +360,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
// on the core, as the scheduled thread will handle the next switch.
if (selectedThread.ThreadContext.Lock())
{
- SwitchTo(selectedThread);
+ SwitchTo(currentThread, selectedThread);
if (!_state.NeedsScheduling)
{
@@ -346,15 +371,15 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
}
else
{
- return _idleThread;
+ return null;
}
}
else
{
// The core is idle now, make sure that the idle thread can run
// and switch the core when a thread is available.
- SwitchTo(null);
- return _idleThread;
+ SwitchTo(currentThread, null);
+ return null;
}
_state.NeedsScheduling = false;
@@ -363,12 +388,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
}
}
- private void SwitchTo(KThread nextThread)
+ private void SwitchTo(KThread currentThread, KThread nextThread)
{
- KProcess currentProcess = KernelStatic.GetCurrentProcess();
- KThread currentThread = KernelStatic.GetCurrentThread();
-
- nextThread ??= _idleThread;
+ KProcess currentProcess = currentThread?.Owner;
if (currentThread != nextThread)
{
@@ -376,7 +398,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
long currentTicks = PerformanceCounter.ElapsedTicks;
long ticksDelta = currentTicks - previousTicks;
- currentThread.AddCpuTime(ticksDelta);
+ if (currentThread == null)
+ {
+ Interlocked.Add(ref _idleTimeRunning, ticksDelta);
+ }
+ else
+ {
+ currentThread.AddCpuTime(ticksDelta);
+ }
currentProcess?.AddCpuTime(ticksDelta);
@@ -386,13 +415,13 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
{
_previousThread = !currentThread.TerminationRequested && currentThread.ActiveCore == _coreId ? currentThread : null;
}
- else if (currentThread == _idleThread)
+ else if (currentThread == null)
{
_previousThread = null;
}
}
- if (nextThread.CurrentCore != _coreId)
+ if (nextThread != null && nextThread.CurrentCore != _coreId)
{
nextThread.CurrentCore = _coreId;
}
@@ -645,11 +674,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
public void Dispose()
{
- // Ensure that the idle thread is not blocked and can exit.
- lock (_idleInterruptEventLock)
- {
- _idleInterruptEvent?.Set();
- }
+ // No resources to dispose for now.
}
}
}
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs
index 12383fb8a..835bf5d40 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs
@@ -143,9 +143,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
PreferredCore = cpuCore;
AffinityMask |= 1UL << cpuCore;
- SchedFlags = type == ThreadType.Dummy
- ? ThreadSchedState.Running
- : ThreadSchedState.None;
+ SchedFlags = ThreadSchedState.None;
ActiveCore = cpuCore;
ObjSyncResult = KernelResult.ThreadNotStarted;
@@ -1055,6 +1053,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
// If the thread is not schedulable, we want to just run or pause
// it directly as we don't care about priority or the core it is
// running on in this case.
+
if (SchedFlags == ThreadSchedState.Running)
{
_schedulerWaitEvent.Set();
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/ThreadType.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/ThreadType.cs
index 83093570b..e2dfd2ffb 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Threading/ThreadType.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/ThreadType.cs
@@ -2,7 +2,6 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
{
enum ThreadType
{
- Dummy,
Kernel,
Kernel2,
User,
From e65effcb05c40247fb717b3c2409abce7ffa10fc Mon Sep 17 00:00:00 2001
From: gdkchan
Date: Thu, 23 May 2024 01:05:32 -0300
Subject: [PATCH 027/109] Workaround AMD bug on logic op with float framebuffer
(#6852)
* Workaround AMD bug on logic op with float framebuffer
* Format whitespace
* Update comment
---
src/Ryujinx.Graphics.GAL/Format.cs | 31 +++++++++++++++++++
.../FramebufferParams.cs | 15 +++++++--
src/Ryujinx.Graphics.Vulkan/PipelineBase.cs | 1 +
.../PipelineConverter.cs | 4 +++
src/Ryujinx.Graphics.Vulkan/PipelineState.cs | 6 +++-
src/Ryujinx.Graphics.Vulkan/PipelineUid.cs | 1 +
6 files changed, 55 insertions(+), 3 deletions(-)
diff --git a/src/Ryujinx.Graphics.GAL/Format.cs b/src/Ryujinx.Graphics.GAL/Format.cs
index bd3b38a9a..17c42d2d4 100644
--- a/src/Ryujinx.Graphics.GAL/Format.cs
+++ b/src/Ryujinx.Graphics.GAL/Format.cs
@@ -711,5 +711,36 @@ namespace Ryujinx.Graphics.GAL
{
return format.IsUint() || format.IsSint();
}
+
+ ///
+ /// Checks if the texture format is a float or sRGB color format.
+ ///
+ ///
+ /// Does not include normalized, compressed or depth formats.
+ /// Float and sRGB formats do not participate in logical operations.
+ ///
+ /// Texture format
+ /// True if the format is a float or sRGB color format, false otherwise
+ public static bool IsFloatOrSrgb(this Format format)
+ {
+ switch (format)
+ {
+ case Format.R8G8B8A8Srgb:
+ case Format.B8G8R8A8Srgb:
+ case Format.R16Float:
+ case Format.R16G16Float:
+ case Format.R16G16B16Float:
+ case Format.R16G16B16A16Float:
+ case Format.R32Float:
+ case Format.R32G32Float:
+ case Format.R32G32B32Float:
+ case Format.R32G32B32A32Float:
+ case Format.R11G11B10Float:
+ case Format.R9G9B9E5Float:
+ return true;
+ }
+
+ return false;
+ }
}
}
diff --git a/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs b/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs
index 8079e5ff9..ea0fd42e5 100644
--- a/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs
+++ b/src/Ryujinx.Graphics.Vulkan/FramebufferParams.cs
@@ -24,6 +24,7 @@ namespace Ryujinx.Graphics.Vulkan
public VkFormat[] AttachmentFormats { get; }
public int[] AttachmentIndices { get; }
public uint AttachmentIntegerFormatMask { get; }
+ public bool LogicOpsAllowed { get; }
public int AttachmentsCount { get; }
public int MaxColorAttachmentIndex => AttachmentIndices.Length > 0 ? AttachmentIndices[^1] : -1;
@@ -32,7 +33,9 @@ namespace Ryujinx.Graphics.Vulkan
public FramebufferParams(Device device, TextureView view, uint width, uint height)
{
- bool isDepthStencil = view.Info.Format.IsDepthOrStencil();
+ var format = view.Info.Format;
+
+ bool isDepthStencil = format.IsDepthOrStencil();
_device = device;
_attachments = new[] { view.GetImageViewForAttachment() };
@@ -56,6 +59,8 @@ namespace Ryujinx.Graphics.Vulkan
AttachmentSamples = new[] { (uint)view.Info.Samples };
AttachmentFormats = new[] { view.VkFormat };
AttachmentIndices = isDepthStencil ? Array.Empty() : new[] { 0 };
+ AttachmentIntegerFormatMask = format.IsInteger() ? 1u : 0u;
+ LogicOpsAllowed = !format.IsFloatOrSrgb();
AttachmentsCount = 1;
@@ -85,6 +90,7 @@ namespace Ryujinx.Graphics.Vulkan
int index = 0;
int bindIndex = 0;
uint attachmentIntegerFormatMask = 0;
+ bool allFormatsFloatOrSrgb = colorsCount != 0;
foreach (ITexture color in colors)
{
@@ -101,11 +107,15 @@ namespace Ryujinx.Graphics.Vulkan
AttachmentFormats[index] = texture.VkFormat;
AttachmentIndices[index] = bindIndex;
- if (texture.Info.Format.IsInteger())
+ var format = texture.Info.Format;
+
+ if (format.IsInteger())
{
attachmentIntegerFormatMask |= 1u << bindIndex;
}
+ allFormatsFloatOrSrgb &= format.IsFloatOrSrgb();
+
width = Math.Min(width, (uint)texture.Width);
height = Math.Min(height, (uint)texture.Height);
layers = Math.Min(layers, (uint)texture.Layers);
@@ -120,6 +130,7 @@ namespace Ryujinx.Graphics.Vulkan
}
AttachmentIntegerFormatMask = attachmentIntegerFormatMask;
+ LogicOpsAllowed = !allFormatsFloatOrSrgb;
if (depthStencil is TextureView dsTexture && dsTexture.Valid)
{
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
index 41ab84d94..3776e2f69 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
@@ -1498,6 +1498,7 @@ namespace Ryujinx.Graphics.Vulkan
var dstAttachmentFormats = _newState.Internal.AttachmentFormats.AsSpan();
FramebufferParams.AttachmentFormats.CopyTo(dstAttachmentFormats);
_newState.Internal.AttachmentIntegerFormatMask = FramebufferParams.AttachmentIntegerFormatMask;
+ _newState.Internal.LogicOpsAllowed = FramebufferParams.LogicOpsAllowed;
for (int i = FramebufferParams.AttachmentFormats.Length; i < dstAttachmentFormats.Length; i++)
{
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs b/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs
index 95b480a5e..41618c736 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs
@@ -302,6 +302,7 @@ namespace Ryujinx.Graphics.Vulkan
int attachmentCount = 0;
int maxColorAttachmentIndex = -1;
uint attachmentIntegerFormatMask = 0;
+ bool allFormatsFloatOrSrgb = true;
for (int i = 0; i < Constants.MaxRenderTargets; i++)
{
@@ -314,6 +315,8 @@ namespace Ryujinx.Graphics.Vulkan
{
attachmentIntegerFormatMask |= 1u << i;
}
+
+ allFormatsFloatOrSrgb &= state.AttachmentFormats[i].IsFloatOrSrgb();
}
}
@@ -325,6 +328,7 @@ namespace Ryujinx.Graphics.Vulkan
pipeline.ColorBlendAttachmentStateCount = (uint)(maxColorAttachmentIndex + 1);
pipeline.VertexAttributeDescriptionsCount = (uint)Math.Min(Constants.MaxVertexAttributes, state.VertexAttribCount);
pipeline.Internal.AttachmentIntegerFormatMask = attachmentIntegerFormatMask;
+ pipeline.Internal.LogicOpsAllowed = attachmentCount == 0 || !allFormatsFloatOrSrgb;
return pipeline;
}
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineState.cs b/src/Ryujinx.Graphics.Vulkan/PipelineState.cs
index 49c12b376..211608584 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineState.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineState.cs
@@ -560,10 +560,14 @@ namespace Ryujinx.Graphics.Vulkan
}
}
+ // AMD has a bug where it enables logical operations even for float formats,
+ // so we need to force disable them here.
+ bool logicOpEnable = LogicOpEnable && (gd.Vendor != Vendor.Amd || Internal.LogicOpsAllowed);
+
var colorBlendState = new PipelineColorBlendStateCreateInfo
{
SType = StructureType.PipelineColorBlendStateCreateInfo,
- LogicOpEnable = LogicOpEnable,
+ LogicOpEnable = logicOpEnable,
LogicOp = LogicOp,
AttachmentCount = ColorBlendAttachmentStateCount,
PAttachments = pColorBlendAttachmentState,
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs b/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs
index 3448d9743..238f06e2a 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs
@@ -34,6 +34,7 @@ namespace Ryujinx.Graphics.Vulkan
public Array8 ColorBlendAttachmentState;
public Array9 AttachmentFormats;
public uint AttachmentIntegerFormatMask;
+ public bool LogicOpsAllowed;
public readonly override bool Equals(object obj)
{
From c98b7fc702deb8d3e93f24d23dfddef375df15ff Mon Sep 17 00:00:00 2001
From: Piplup <100526773+piplup55@users.noreply.github.com>
Date: Fri, 24 May 2024 02:57:26 +0100
Subject: [PATCH 028/109] Workaround bug on logic op with float framebuffer
(#6858)
* intel workaround
built on top of the amd workaround
* forgot to update the note
* Logic Change
Enabled workaround for all vendors that aren't nvidia
* Applied Suggestions
---
src/Ryujinx.Graphics.Vulkan/PipelineState.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineState.cs b/src/Ryujinx.Graphics.Vulkan/PipelineState.cs
index 211608584..c38748936 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineState.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineState.cs
@@ -560,9 +560,9 @@ namespace Ryujinx.Graphics.Vulkan
}
}
- // AMD has a bug where it enables logical operations even for float formats,
+ // Vendors other than NVIDIA have a bug where it enables logical operations even for float formats,
// so we need to force disable them here.
- bool logicOpEnable = LogicOpEnable && (gd.Vendor != Vendor.Amd || Internal.LogicOpsAllowed);
+ bool logicOpEnable = LogicOpEnable && (gd.Vendor == Vendor.Nvidia || Internal.LogicOpsAllowed);
var colorBlendState = new PipelineColorBlendStateCreateInfo
{
From 4cc00bb4b1b777734151cab5570d622fbfefa49f Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 25 May 2024 05:35:49 +0200
Subject: [PATCH 029/109] nuget: bump Microsoft.IdentityModel.JsonWebTokens
from 7.5.1 to 7.5.2 (#6809)
Bumps [Microsoft.IdentityModel.JsonWebTokens](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet) from 7.5.1 to 7.5.2.
- [Release notes](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/releases)
- [Changelog](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/compare/7.5.1...7.5.2)
---
updated-dependencies:
- dependency-name: Microsoft.IdentityModel.JsonWebTokens
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Directory.Packages.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 739e66bd0..a93247547 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -20,7 +20,7 @@
-
+
From 53d096e392d85106a41d8edad1dcda5cce7446a2 Mon Sep 17 00:00:00 2001
From: gdkchan
Date: Sun, 26 May 2024 13:30:19 -0300
Subject: [PATCH 030/109] Allow texture arrays to use separate descriptor sets
on Vulkan (#6870)
* Report base and extra sets from the backend
* Pass texture set index everywhere
* Key textures using set and binding (rather than just binding)
* Start using extra sets for array textures
* Shader cache version bump
* Separate new commands, some PR feedback
* Introduce new manual descriptor set reservation method that prevents it from being used by something else while owned by an array
* Move bind extra sets logic to new method
* Should only use separate array is MaximumExtraSets is not zero
* Format whitespace
---
src/Ryujinx.Graphics.GAL/Capabilities.cs | 19 ++
src/Ryujinx.Graphics.GAL/IPipeline.cs | 2 +
.../Multithreading/CommandHelper.cs | 2 +
.../Multithreading/CommandType.cs | 2 +
.../Commands/SetImageArraySeparateCommand.cs | 26 +++
.../SetTextureArraySeparateCommand.cs | 26 +++
.../Multithreading/ThreadedPipeline.cs | 12 ++
.../Image/TextureBindingInfo.cs | 13 +-
.../Image/TextureBindingsArrayCache.cs | 70 +++++--
.../Shader/CachedShaderBindings.cs | 2 +
.../Shader/DiskCache/DiskCacheHostStorage.cs | 2 +-
.../Shader/GpuAccessorBase.cs | 26 ++-
.../Shader/ResourceCounts.cs | 5 +
.../Shader/ShaderInfoBuilder.cs | 144 ++++++++++----
src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs | 6 +
src/Ryujinx.Graphics.OpenGL/Pipeline.cs | 10 +
.../BufferDescriptor.cs | 7 +-
.../Glsl/Instructions/InstGenMemory.cs | 8 +-
.../CodeGen/Spirv/CodeGenContext.cs | 6 +-
.../CodeGen/Spirv/Declarations.cs | 6 +-
.../CodeGen/Spirv/Instructions.cs | 18 +-
src/Ryujinx.Graphics.Shader/IGpuAccessor.cs | 25 ++-
.../Instructions/InstEmitSurface.cs | 20 +-
.../Instructions/InstEmitTexture.cs | 18 +-
.../TextureOperation.cs | 22 ++-
src/Ryujinx.Graphics.Shader/SetBindingPair.cs | 4 +
.../StructuredIr/AstTextureOperation.cs | 16 ++
.../StructuredIr/ShaderProperties.cs | 16 +-
.../StructuredIr/StructuredProgram.cs | 12 +-
.../TextureDescriptor.cs | 3 +
.../Translation/EmitterContext.cs | 4 +-
.../Translation/EmitterContextInsts.cs | 91 +++++++--
.../Optimizations/BindlessElimination.cs | 12 +-
.../Optimizations/BindlessToArray.cs | 4 +-
.../Translation/ResourceManager.cs | 98 ++++++----
.../Translation/ResourceReservations.cs | 17 ++
.../Translation/Transforms/TexturePass.cs | 7 +
.../Translation/Transforms/VertexToCompute.cs | 8 +-
.../Translation/TranslatorContext.cs | 12 +-
.../DescriptorSetUpdater.cs | 178 +++++++++++++-----
src/Ryujinx.Graphics.Vulkan/ImageArray.cs | 75 +++++++-
src/Ryujinx.Graphics.Vulkan/PipelineBase.cs | 16 +-
.../PipelineLayoutCacheEntry.cs | 80 ++++++++
.../ShaderCollection.cs | 10 +
src/Ryujinx.Graphics.Vulkan/TextureArray.cs | 76 +++++++-
src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs | 6 +
src/Ryujinx.ShaderTools/Program.cs | 16 +-
47 files changed, 996 insertions(+), 262 deletions(-)
create mode 100644 src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetImageArraySeparateCommand.cs
create mode 100644 src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetTextureArraySeparateCommand.cs
create mode 100644 src/Ryujinx.Graphics.Shader/SetBindingPair.cs
diff --git a/src/Ryujinx.Graphics.GAL/Capabilities.cs b/src/Ryujinx.Graphics.GAL/Capabilities.cs
index d758586ae..a5c6eb5c8 100644
--- a/src/Ryujinx.Graphics.GAL/Capabilities.cs
+++ b/src/Ryujinx.Graphics.GAL/Capabilities.cs
@@ -51,6 +51,13 @@ namespace Ryujinx.Graphics.GAL
public readonly bool SupportsIndirectParameters;
public readonly bool SupportsDepthClipControl;
+ public readonly int UniformBufferSetIndex;
+ public readonly int StorageBufferSetIndex;
+ public readonly int TextureSetIndex;
+ public readonly int ImageSetIndex;
+ public readonly int ExtraSetBaseIndex;
+ public readonly int MaximumExtraSets;
+
public readonly uint MaximumUniformBuffersPerStage;
public readonly uint MaximumStorageBuffersPerStage;
public readonly uint MaximumTexturesPerStage;
@@ -109,6 +116,12 @@ namespace Ryujinx.Graphics.GAL
bool supportsViewportSwizzle,
bool supportsIndirectParameters,
bool supportsDepthClipControl,
+ int uniformBufferSetIndex,
+ int storageBufferSetIndex,
+ int textureSetIndex,
+ int imageSetIndex,
+ int extraSetBaseIndex,
+ int maximumExtraSets,
uint maximumUniformBuffersPerStage,
uint maximumStorageBuffersPerStage,
uint maximumTexturesPerStage,
@@ -164,6 +177,12 @@ namespace Ryujinx.Graphics.GAL
SupportsViewportSwizzle = supportsViewportSwizzle;
SupportsIndirectParameters = supportsIndirectParameters;
SupportsDepthClipControl = supportsDepthClipControl;
+ UniformBufferSetIndex = uniformBufferSetIndex;
+ StorageBufferSetIndex = storageBufferSetIndex;
+ TextureSetIndex = textureSetIndex;
+ ImageSetIndex = imageSetIndex;
+ ExtraSetBaseIndex = extraSetBaseIndex;
+ MaximumExtraSets = maximumExtraSets;
MaximumUniformBuffersPerStage = maximumUniformBuffersPerStage;
MaximumStorageBuffersPerStage = maximumStorageBuffersPerStage;
MaximumTexturesPerStage = maximumTexturesPerStage;
diff --git a/src/Ryujinx.Graphics.GAL/IPipeline.cs b/src/Ryujinx.Graphics.GAL/IPipeline.cs
index 9efb9e3e8..cbf1bc3a2 100644
--- a/src/Ryujinx.Graphics.GAL/IPipeline.cs
+++ b/src/Ryujinx.Graphics.GAL/IPipeline.cs
@@ -60,6 +60,7 @@ namespace Ryujinx.Graphics.GAL
void SetImage(ShaderStage stage, int binding, ITexture texture, Format imageFormat);
void SetImageArray(ShaderStage stage, int binding, IImageArray array);
+ void SetImageArraySeparate(ShaderStage stage, int setIndex, IImageArray array);
void SetLineParameters(float width, bool smooth);
@@ -91,6 +92,7 @@ namespace Ryujinx.Graphics.GAL
void SetTextureAndSampler(ShaderStage stage, int binding, ITexture texture, ISampler sampler);
void SetTextureArray(ShaderStage stage, int binding, ITextureArray array);
+ void SetTextureArraySeparate(ShaderStage stage, int setIndex, ITextureArray array);
void SetTransformFeedbackBuffers(ReadOnlySpan buffers);
void SetUniformBuffers(ReadOnlySpan buffers);
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs b/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
index 23f1a64ef..edaae3042 100644
--- a/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
@@ -124,6 +124,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
Register(CommandType.SetUniformBuffers);
Register(CommandType.SetImage);
Register(CommandType.SetImageArray);
+ Register(CommandType.SetImageArraySeparate);
Register(CommandType.SetIndexBuffer);
Register(CommandType.SetLineParameters);
Register(CommandType.SetLogicOpState);
@@ -141,6 +142,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
Register(CommandType.SetStencilTest);
Register(CommandType.SetTextureAndSampler);
Register(CommandType.SetTextureArray);
+ Register(CommandType.SetTextureArraySeparate);
Register(CommandType.SetUserClipDistance);
Register(CommandType.SetVertexAttribs);
Register(CommandType.SetVertexBuffers);
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs b/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs
index f95aab05b..758695352 100644
--- a/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs
@@ -84,6 +84,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
SetUniformBuffers,
SetImage,
SetImageArray,
+ SetImageArraySeparate,
SetIndexBuffer,
SetLineParameters,
SetLogicOpState,
@@ -101,6 +102,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
SetStencilTest,
SetTextureAndSampler,
SetTextureArray,
+ SetTextureArraySeparate,
SetUserClipDistance,
SetVertexAttribs,
SetVertexBuffers,
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetImageArraySeparateCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetImageArraySeparateCommand.cs
new file mode 100644
index 000000000..abeb58a06
--- /dev/null
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetImageArraySeparateCommand.cs
@@ -0,0 +1,26 @@
+using Ryujinx.Graphics.GAL.Multithreading.Model;
+using Ryujinx.Graphics.GAL.Multithreading.Resources;
+using Ryujinx.Graphics.Shader;
+
+namespace Ryujinx.Graphics.GAL.Multithreading.Commands
+{
+ struct SetImageArraySeparateCommand : IGALCommand, IGALCommand
+ {
+ public readonly CommandType CommandType => CommandType.SetImageArraySeparate;
+ private ShaderStage _stage;
+ private int _setIndex;
+ private TableRef _array;
+
+ public void Set(ShaderStage stage, int setIndex, TableRef array)
+ {
+ _stage = stage;
+ _setIndex = setIndex;
+ _array = array;
+ }
+
+ public static void Run(ref SetImageArraySeparateCommand command, ThreadedRenderer threaded, IRenderer renderer)
+ {
+ renderer.Pipeline.SetImageArraySeparate(command._stage, command._setIndex, command._array.GetAs(threaded)?.Base);
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetTextureArraySeparateCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetTextureArraySeparateCommand.cs
new file mode 100644
index 000000000..b179f2e70
--- /dev/null
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/SetTextureArraySeparateCommand.cs
@@ -0,0 +1,26 @@
+using Ryujinx.Graphics.GAL.Multithreading.Model;
+using Ryujinx.Graphics.GAL.Multithreading.Resources;
+using Ryujinx.Graphics.Shader;
+
+namespace Ryujinx.Graphics.GAL.Multithreading.Commands
+{
+ struct SetTextureArraySeparateCommand : IGALCommand, IGALCommand
+ {
+ public readonly CommandType CommandType => CommandType.SetTextureArraySeparate;
+ private ShaderStage _stage;
+ private int _setIndex;
+ private TableRef _array;
+
+ public void Set(ShaderStage stage, int setIndex, TableRef array)
+ {
+ _stage = stage;
+ _setIndex = setIndex;
+ _array = array;
+ }
+
+ public static void Run(ref SetTextureArraySeparateCommand command, ThreadedRenderer threaded, IRenderer renderer)
+ {
+ renderer.Pipeline.SetTextureArraySeparate(command._stage, command._setIndex, command._array.GetAs(threaded)?.Base);
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs
index 697894eb5..edd79d8a0 100644
--- a/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/ThreadedPipeline.cs
@@ -189,6 +189,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading
_renderer.QueueCommand();
}
+ public void SetImageArraySeparate(ShaderStage stage, int setIndex, IImageArray array)
+ {
+ _renderer.New().Set(stage, setIndex, Ref(array));
+ _renderer.QueueCommand();
+ }
+
public void SetIndexBuffer(BufferRange buffer, IndexType type)
{
_renderer.New().Set(buffer, type);
@@ -297,6 +303,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading
_renderer.QueueCommand();
}
+ public void SetTextureArraySeparate(ShaderStage stage, int setIndex, ITextureArray array)
+ {
+ _renderer.New().Set(stage, setIndex, Ref(array));
+ _renderer.QueueCommand();
+ }
+
public void SetTransformFeedbackBuffers(ReadOnlySpan buffers)
{
_renderer.New().Set(_renderer.CopySpan(buffers));
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs
index ba895c60a..31abc21e8 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingInfo.cs
@@ -19,6 +19,11 @@ namespace Ryujinx.Graphics.Gpu.Image
///
public Format Format { get; }
+ ///
+ /// Shader texture host set index.
+ ///
+ public int Set { get; }
+
///
/// Shader texture host binding point.
///
@@ -54,15 +59,17 @@ namespace Ryujinx.Graphics.Gpu.Image
///
/// The shader sampler target type
/// Format of the image as declared on the shader
+ /// Shader texture host set index
/// The shader texture binding point
/// For array of textures, this indicates the length of the array. A value of one indicates it is not an array
/// Constant buffer slot where the texture handle is located
/// The shader texture handle (read index into the texture constant buffer)
/// The texture's usage flags, indicating how it is used in the shader
- public TextureBindingInfo(Target target, Format format, int binding, int arrayLength, int cbufSlot, int handle, TextureUsageFlags flags)
+ public TextureBindingInfo(Target target, Format format, int set, int binding, int arrayLength, int cbufSlot, int handle, TextureUsageFlags flags)
{
Target = target;
Format = format;
+ Set = set;
Binding = binding;
ArrayLength = arrayLength;
CbufSlot = cbufSlot;
@@ -74,6 +81,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// Constructs the texture binding information structure.
///
/// The shader sampler target type
+ /// Shader texture host set index
/// The shader texture binding point
/// For array of textures, this indicates the length of the array. A value of one indicates it is not an array
/// Constant buffer slot where the texture handle is located
@@ -82,12 +90,13 @@ namespace Ryujinx.Graphics.Gpu.Image
/// Indicates that the binding is for a sampler
public TextureBindingInfo(
Target target,
+ int set,
int binding,
int arrayLength,
int cbufSlot,
int handle,
TextureUsageFlags flags,
- bool isSamplerOnly) : this(target, 0, binding, arrayLength, cbufSlot, handle, flags)
+ bool isSamplerOnly) : this(target, 0, set, binding, arrayLength, cbufSlot, handle, flags)
{
IsSamplerOnly = isSamplerOnly;
}
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs
index a54d07000..18e28b3dd 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs
@@ -566,7 +566,7 @@ namespace Ryujinx.Graphics.Gpu.Image
int stageIndex,
int textureBufferIndex,
SamplerIndex samplerIndex,
- TextureBindingInfo bindingInfo)
+ in TextureBindingInfo bindingInfo)
{
Update(texturePool, samplerPool, stage, stageIndex, textureBufferIndex, isImage: false, samplerIndex, bindingInfo);
}
@@ -579,7 +579,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// Shader stage index where the array is used
/// Texture constant buffer index
/// Array binding information
- public void UpdateImageArray(TexturePool texturePool, ShaderStage stage, int stageIndex, int textureBufferIndex, TextureBindingInfo bindingInfo)
+ public void UpdateImageArray(TexturePool texturePool, ShaderStage stage, int stageIndex, int textureBufferIndex, in TextureBindingInfo bindingInfo)
{
Update(texturePool, null, stage, stageIndex, textureBufferIndex, isImage: true, SamplerIndex.ViaHeaderIndex, bindingInfo);
}
@@ -603,7 +603,7 @@ namespace Ryujinx.Graphics.Gpu.Image
int textureBufferIndex,
bool isImage,
SamplerIndex samplerIndex,
- TextureBindingInfo bindingInfo)
+ in TextureBindingInfo bindingInfo)
{
if (IsDirectHandleType(bindingInfo.Handle))
{
@@ -623,7 +623,7 @@ namespace Ryujinx.Graphics.Gpu.Image
/// Shader stage where the array is used
/// Whether the array is a image or texture array
/// Array binding information
- private void UpdateFromPool(TexturePool texturePool, SamplerPool samplerPool, ShaderStage stage, bool isImage, TextureBindingInfo bindingInfo)
+ private void UpdateFromPool(TexturePool texturePool, SamplerPool samplerPool, ShaderStage stage, bool isImage, in TextureBindingInfo bindingInfo)
{
CacheEntry entry = GetOrAddEntry(texturePool, samplerPool, bindingInfo, isImage, out bool isNewEntry);
@@ -638,11 +638,11 @@ namespace Ryujinx.Graphics.Gpu.Image
if (isImage)
{
- _context.Renderer.Pipeline.SetImageArray(stage, bindingInfo.Binding, entry.ImageArray);
+ SetImageArray(stage, bindingInfo, entry.ImageArray);
}
else
{
- _context.Renderer.Pipeline.SetTextureArray(stage, bindingInfo.Binding, entry.TextureArray);
+ SetTextureArray(stage, bindingInfo, entry.TextureArray);
}
return;
@@ -737,14 +737,14 @@ namespace Ryujinx.Graphics.Gpu.Image
entry.ImageArray.SetFormats(0, formats);
entry.ImageArray.SetImages(0, textures);
- _context.Renderer.Pipeline.SetImageArray(stage, bindingInfo.Binding, entry.ImageArray);
+ SetImageArray(stage, bindingInfo, entry.ImageArray);
}
else
{
entry.TextureArray.SetSamplers(0, samplers);
entry.TextureArray.SetTextures(0, textures);
- _context.Renderer.Pipeline.SetTextureArray(stage, bindingInfo.Binding, entry.TextureArray);
+ SetTextureArray(stage, bindingInfo, entry.TextureArray);
}
}
@@ -767,7 +767,7 @@ namespace Ryujinx.Graphics.Gpu.Image
int textureBufferIndex,
bool isImage,
SamplerIndex samplerIndex,
- TextureBindingInfo bindingInfo)
+ in TextureBindingInfo bindingInfo)
{
(textureBufferIndex, int samplerBufferIndex) = TextureHandle.UnpackSlots(bindingInfo.CbufSlot, textureBufferIndex);
@@ -800,11 +800,11 @@ namespace Ryujinx.Graphics.Gpu.Image
if (isImage)
{
- _context.Renderer.Pipeline.SetImageArray(stage, bindingInfo.Binding, entry.ImageArray);
+ SetImageArray(stage, bindingInfo, entry.ImageArray);
}
else
{
- _context.Renderer.Pipeline.SetTextureArray(stage, bindingInfo.Binding, entry.TextureArray);
+ SetTextureArray(stage, bindingInfo, entry.TextureArray);
}
return;
@@ -829,11 +829,11 @@ namespace Ryujinx.Graphics.Gpu.Image
if (isImage)
{
- _context.Renderer.Pipeline.SetImageArray(stage, bindingInfo.Binding, entry.ImageArray);
+ SetImageArray(stage, bindingInfo, entry.ImageArray);
}
else
{
- _context.Renderer.Pipeline.SetTextureArray(stage, bindingInfo.Binding, entry.TextureArray);
+ SetTextureArray(stage, bindingInfo, entry.TextureArray);
}
return;
@@ -950,14 +950,50 @@ namespace Ryujinx.Graphics.Gpu.Image
entry.ImageArray.SetFormats(0, formats);
entry.ImageArray.SetImages(0, textures);
- _context.Renderer.Pipeline.SetImageArray(stage, bindingInfo.Binding, entry.ImageArray);
+ SetImageArray(stage, bindingInfo, entry.ImageArray);
}
else
{
entry.TextureArray.SetSamplers(0, samplers);
entry.TextureArray.SetTextures(0, textures);
- _context.Renderer.Pipeline.SetTextureArray(stage, bindingInfo.Binding, entry.TextureArray);
+ SetTextureArray(stage, bindingInfo, entry.TextureArray);
+ }
+ }
+
+ ///
+ /// Updates a texture array binding on the host.
+ ///
+ /// Shader stage where the array is used
+ /// Array binding information
+ /// Texture array
+ private void SetTextureArray(ShaderStage stage, in TextureBindingInfo bindingInfo, ITextureArray array)
+ {
+ if (bindingInfo.Set >= _context.Capabilities.ExtraSetBaseIndex && _context.Capabilities.MaximumExtraSets != 0)
+ {
+ _context.Renderer.Pipeline.SetTextureArraySeparate(stage, bindingInfo.Set, array);
+ }
+ else
+ {
+ _context.Renderer.Pipeline.SetTextureArray(stage, bindingInfo.Binding, array);
+ }
+ }
+
+ ///
+ /// Updates a image array binding on the host.
+ ///
+ /// Shader stage where the array is used
+ /// Array binding information
+ /// Image array
+ private void SetImageArray(ShaderStage stage, in TextureBindingInfo bindingInfo, IImageArray array)
+ {
+ if (bindingInfo.Set >= _context.Capabilities.ExtraSetBaseIndex && _context.Capabilities.MaximumExtraSets != 0)
+ {
+ _context.Renderer.Pipeline.SetImageArraySeparate(stage, bindingInfo.Set, array);
+ }
+ else
+ {
+ _context.Renderer.Pipeline.SetImageArray(stage, bindingInfo.Binding, array);
}
}
@@ -973,7 +1009,7 @@ namespace Ryujinx.Graphics.Gpu.Image
private CacheEntry GetOrAddEntry(
TexturePool texturePool,
SamplerPool samplerPool,
- TextureBindingInfo bindingInfo,
+ in TextureBindingInfo bindingInfo,
bool isImage,
out bool isNew)
{
@@ -1015,7 +1051,7 @@ namespace Ryujinx.Graphics.Gpu.Image
private CacheEntryFromBuffer GetOrAddEntry(
TexturePool texturePool,
SamplerPool samplerPool,
- TextureBindingInfo bindingInfo,
+ in TextureBindingInfo bindingInfo,
bool isImage,
ref BufferBounds textureBufferBounds,
out bool isNew)
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderBindings.cs b/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderBindings.cs
index a80dcbc87..51be00b6e 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderBindings.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/CachedShaderBindings.cs
@@ -62,6 +62,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
var result = new TextureBindingInfo(
target,
+ descriptor.Set,
descriptor.Binding,
descriptor.ArrayLength,
descriptor.CbufSlot,
@@ -90,6 +91,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
var result = new TextureBindingInfo(
target,
format,
+ descriptor.Set,
descriptor.Binding,
descriptor.ArrayLength,
descriptor.CbufSlot,
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
index ea54049c2..990c6ba3b 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
@@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
private const ushort FileFormatVersionMajor = 1;
private const ushort FileFormatVersionMinor = 2;
private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
- private const uint CodeGenVersion = 5936;
+ private const uint CodeGenVersion = 6870;
private const string SharedTocFileName = "shared.toc";
private const string SharedDataFileName = "shared.data";
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs
index 0d562b0da..d89eebabf 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs
@@ -51,7 +51,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
_reservedImages = rrc.ReservedImages;
}
- public int CreateConstantBufferBinding(int index)
+ public SetBindingPair CreateConstantBufferBinding(int index)
{
int binding;
@@ -64,10 +64,10 @@ namespace Ryujinx.Graphics.Gpu.Shader
binding = _resourceCounts.UniformBuffersCount++;
}
- return binding + _reservedConstantBuffers;
+ return new SetBindingPair(_context.Capabilities.UniformBufferSetIndex, binding + _reservedConstantBuffers);
}
- public int CreateImageBinding(int count, bool isBuffer)
+ public SetBindingPair CreateImageBinding(int count, bool isBuffer)
{
int binding;
@@ -96,10 +96,10 @@ namespace Ryujinx.Graphics.Gpu.Shader
_resourceCounts.ImagesCount += count;
}
- return binding + _reservedImages;
+ return new SetBindingPair(_context.Capabilities.ImageSetIndex, binding + _reservedImages);
}
- public int CreateStorageBufferBinding(int index)
+ public SetBindingPair CreateStorageBufferBinding(int index)
{
int binding;
@@ -112,10 +112,10 @@ namespace Ryujinx.Graphics.Gpu.Shader
binding = _resourceCounts.StorageBuffersCount++;
}
- return binding + _reservedStorageBuffers;
+ return new SetBindingPair(_context.Capabilities.StorageBufferSetIndex, binding + _reservedStorageBuffers);
}
- public int CreateTextureBinding(int count, bool isBuffer)
+ public SetBindingPair CreateTextureBinding(int count, bool isBuffer)
{
int binding;
@@ -144,7 +144,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
_resourceCounts.TexturesCount += count;
}
- return binding + _reservedTextures;
+ return new SetBindingPair(_context.Capabilities.TextureSetIndex, binding + _reservedTextures);
}
private int GetBindingFromIndex(int index, uint maxPerStage, string resourceName)
@@ -183,6 +183,16 @@ namespace Ryujinx.Graphics.Gpu.Shader
return maxPerStage * Constants.ShaderStages;
}
+ public int CreateExtraSet()
+ {
+ if (_resourceCounts.SetsCount >= _context.Capabilities.MaximumExtraSets)
+ {
+ return -1;
+ }
+
+ return _context.Capabilities.ExtraSetBaseIndex + _resourceCounts.SetsCount++;
+ }
+
public int QueryHostGatherBiasPrecision() => _context.Capabilities.GatherBiasPrecision;
public bool QueryHostReducedPrecision() => _context.Capabilities.ReduceShaderPrecision;
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ResourceCounts.cs b/src/Ryujinx.Graphics.Gpu/Shader/ResourceCounts.cs
index 126e3249c..59ab378cf 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/ResourceCounts.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/ResourceCounts.cs
@@ -24,5 +24,10 @@ namespace Ryujinx.Graphics.Gpu.Shader
/// Total of images used by the shaders.
///
public int ImagesCount;
+
+ ///
+ /// Total of extra sets used by the shaders.
+ ///
+ public int SetsCount;
}
}
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs
index ed56db3b3..42b2cbb59 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs
@@ -1,5 +1,6 @@
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.Shader;
+using System;
using System.Collections.Generic;
namespace Ryujinx.Graphics.Gpu.Shader
@@ -9,13 +10,6 @@ namespace Ryujinx.Graphics.Gpu.Shader
///
class ShaderInfoBuilder
{
- private const int TotalSets = 4;
-
- private const int UniformSetIndex = 0;
- private const int StorageSetIndex = 1;
- private const int TextureSetIndex = 2;
- private const int ImageSetIndex = 3;
-
private const ResourceStages SupportBufferStages =
ResourceStages.Compute |
ResourceStages.Vertex |
@@ -36,8 +30,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
private readonly int _reservedTextures;
private readonly int _reservedImages;
- private readonly List[] _resourceDescriptors;
- private readonly List[] _resourceUsages;
+ private List[] _resourceDescriptors;
+ private List[] _resourceUsages;
///
/// Creates a new shader info builder.
@@ -51,17 +45,27 @@ namespace Ryujinx.Graphics.Gpu.Shader
_fragmentOutputMap = -1;
- _resourceDescriptors = new List[TotalSets];
- _resourceUsages = new List[TotalSets];
+ int uniformSetIndex = context.Capabilities.UniformBufferSetIndex;
+ int storageSetIndex = context.Capabilities.StorageBufferSetIndex;
+ int textureSetIndex = context.Capabilities.TextureSetIndex;
+ int imageSetIndex = context.Capabilities.ImageSetIndex;
- for (int index = 0; index < TotalSets; index++)
+ int totalSets = Math.Max(uniformSetIndex, storageSetIndex);
+ totalSets = Math.Max(totalSets, textureSetIndex);
+ totalSets = Math.Max(totalSets, imageSetIndex);
+ totalSets++;
+
+ _resourceDescriptors = new List[totalSets];
+ _resourceUsages = new List[totalSets];
+
+ for (int index = 0; index < totalSets; index++)
{
_resourceDescriptors[index] = new();
_resourceUsages[index] = new();
}
- AddDescriptor(SupportBufferStages, ResourceType.UniformBuffer, UniformSetIndex, 0, 1);
- AddUsage(SupportBufferStages, ResourceType.UniformBuffer, UniformSetIndex, 0, 1);
+ AddDescriptor(SupportBufferStages, ResourceType.UniformBuffer, uniformSetIndex, 0, 1);
+ AddUsage(SupportBufferStages, ResourceType.UniformBuffer, uniformSetIndex, 0, 1);
ResourceReservationCounts rrc = new(!context.Capabilities.SupportsTransformFeedback && tfEnabled, vertexAsCompute);
@@ -73,12 +77,20 @@ namespace Ryujinx.Graphics.Gpu.Shader
// TODO: Handle that better? Maybe we should only set the binding that are really needed on each shader.
ResourceStages stages = vertexAsCompute ? ResourceStages.Compute | ResourceStages.Vertex : VtgStages;
- PopulateDescriptorAndUsages(stages, ResourceType.UniformBuffer, UniformSetIndex, 1, rrc.ReservedConstantBuffers - 1);
- PopulateDescriptorAndUsages(stages, ResourceType.StorageBuffer, StorageSetIndex, 0, rrc.ReservedStorageBuffers);
- PopulateDescriptorAndUsages(stages, ResourceType.BufferTexture, TextureSetIndex, 0, rrc.ReservedTextures);
- PopulateDescriptorAndUsages(stages, ResourceType.BufferImage, ImageSetIndex, 0, rrc.ReservedImages);
+ PopulateDescriptorAndUsages(stages, ResourceType.UniformBuffer, uniformSetIndex, 1, rrc.ReservedConstantBuffers - 1);
+ PopulateDescriptorAndUsages(stages, ResourceType.StorageBuffer, storageSetIndex, 0, rrc.ReservedStorageBuffers);
+ PopulateDescriptorAndUsages(stages, ResourceType.BufferTexture, textureSetIndex, 0, rrc.ReservedTextures);
+ PopulateDescriptorAndUsages(stages, ResourceType.BufferImage, imageSetIndex, 0, rrc.ReservedImages);
}
+ ///
+ /// Populates descriptors and usages for vertex as compute and transform feedback emulation reserved resources.
+ ///
+ /// Shader stages where the resources are used
+ /// Resource type
+ /// Resource set index where the resources are used
+ /// First binding number
+ /// Amount of bindings
private void PopulateDescriptorAndUsages(ResourceStages stages, ResourceType type, int setIndex, int start, int count)
{
AddDescriptor(stages, type, setIndex, start, count);
@@ -127,18 +139,23 @@ namespace Ryujinx.Graphics.Gpu.Shader
int textureBinding = _reservedTextures + stageIndex * texturesPerStage * 2;
int imageBinding = _reservedImages + stageIndex * imagesPerStage * 2;
- AddDescriptor(stages, ResourceType.UniformBuffer, UniformSetIndex, uniformBinding, uniformsPerStage);
- AddDescriptor(stages, ResourceType.StorageBuffer, StorageSetIndex, storageBinding, storagesPerStage);
- AddDualDescriptor(stages, ResourceType.TextureAndSampler, ResourceType.BufferTexture, TextureSetIndex, textureBinding, texturesPerStage);
- AddDualDescriptor(stages, ResourceType.Image, ResourceType.BufferImage, ImageSetIndex, imageBinding, imagesPerStage);
+ int uniformSetIndex = _context.Capabilities.UniformBufferSetIndex;
+ int storageSetIndex = _context.Capabilities.StorageBufferSetIndex;
+ int textureSetIndex = _context.Capabilities.TextureSetIndex;
+ int imageSetIndex = _context.Capabilities.ImageSetIndex;
- AddArrayDescriptors(info.Textures, stages, TextureSetIndex, isImage: false);
- AddArrayDescriptors(info.Images, stages, TextureSetIndex, isImage: true);
+ AddDescriptor(stages, ResourceType.UniformBuffer, uniformSetIndex, uniformBinding, uniformsPerStage);
+ AddDescriptor(stages, ResourceType.StorageBuffer, storageSetIndex, storageBinding, storagesPerStage);
+ AddDualDescriptor(stages, ResourceType.TextureAndSampler, ResourceType.BufferTexture, textureSetIndex, textureBinding, texturesPerStage);
+ AddDualDescriptor(stages, ResourceType.Image, ResourceType.BufferImage, imageSetIndex, imageBinding, imagesPerStage);
- AddUsage(info.CBuffers, stages, UniformSetIndex, isStorage: false);
- AddUsage(info.SBuffers, stages, StorageSetIndex, isStorage: true);
- AddUsage(info.Textures, stages, TextureSetIndex, isImage: false);
- AddUsage(info.Images, stages, ImageSetIndex, isImage: true);
+ AddArrayDescriptors(info.Textures, stages, isImage: false);
+ AddArrayDescriptors(info.Images, stages, isImage: true);
+
+ AddUsage(info.CBuffers, stages, isStorage: false);
+ AddUsage(info.SBuffers, stages, isStorage: true);
+ AddUsage(info.Textures, stages, isImage: false);
+ AddUsage(info.Images, stages, isImage: true);
}
///
@@ -177,9 +194,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
///
/// Textures to be added
/// Stages where the textures are used
- /// Descriptor set index where the textures will be bound
/// True for images, false for textures
- private void AddArrayDescriptors(IEnumerable textures, ResourceStages stages, int setIndex, bool isImage)
+ private void AddArrayDescriptors(IEnumerable textures, ResourceStages stages, bool isImage)
{
foreach (TextureDescriptor texture in textures)
{
@@ -187,7 +203,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
{
ResourceType type = GetTextureResourceType(texture, isImage);
- _resourceDescriptors[setIndex].Add(new ResourceDescriptor(texture.Binding, texture.ArrayLength, type, stages));
+ GetDescriptors(texture.Set).Add(new ResourceDescriptor(texture.Binding, texture.ArrayLength, type, stages));
}
}
}
@@ -213,13 +229,12 @@ namespace Ryujinx.Graphics.Gpu.Shader
///
/// Buffers to be added
/// Stages where the buffers are used
- /// Descriptor set index where the buffers will be bound
/// True for storage buffers, false for uniform buffers
- private void AddUsage(IEnumerable buffers, ResourceStages stages, int setIndex, bool isStorage)
+ private void AddUsage(IEnumerable buffers, ResourceStages stages, bool isStorage)
{
foreach (BufferDescriptor buffer in buffers)
{
- _resourceUsages[setIndex].Add(new ResourceUsage(
+ GetUsages(buffer.Set).Add(new ResourceUsage(
buffer.Binding,
1,
isStorage ? ResourceType.StorageBuffer : ResourceType.UniformBuffer,
@@ -232,18 +247,65 @@ namespace Ryujinx.Graphics.Gpu.Shader
///
/// Textures to be added
/// Stages where the textures are used
- /// Descriptor set index where the textures will be bound
/// True for images, false for textures
- private void AddUsage(IEnumerable textures, ResourceStages stages, int setIndex, bool isImage)
+ private void AddUsage(IEnumerable textures, ResourceStages stages, bool isImage)
{
foreach (TextureDescriptor texture in textures)
{
ResourceType type = GetTextureResourceType(texture, isImage);
- _resourceUsages[setIndex].Add(new ResourceUsage(texture.Binding, texture.ArrayLength, type, stages));
+ GetUsages(texture.Set).Add(new ResourceUsage(texture.Binding, texture.ArrayLength, type, stages));
}
}
+ ///
+ /// Gets the list of resource descriptors for a given set index. A new list will be created if needed.
+ ///
+ /// Resource set index
+ /// List of resource descriptors
+ private List GetDescriptors(int setIndex)
+ {
+ if (_resourceDescriptors.Length <= setIndex)
+ {
+ int oldLength = _resourceDescriptors.Length;
+ Array.Resize(ref _resourceDescriptors, setIndex + 1);
+
+ for (int index = oldLength; index <= setIndex; index++)
+ {
+ _resourceDescriptors[index] = new();
+ }
+ }
+
+ return _resourceDescriptors[setIndex];
+ }
+
+ ///
+ /// Gets the list of resource usages for a given set index. A new list will be created if needed.
+ ///
+ /// Resource set index
+ /// List of resource usages
+ private List GetUsages(int setIndex)
+ {
+ if (_resourceUsages.Length <= setIndex)
+ {
+ int oldLength = _resourceUsages.Length;
+ Array.Resize(ref _resourceUsages, setIndex + 1);
+
+ for (int index = oldLength; index <= setIndex; index++)
+ {
+ _resourceUsages[index] = new();
+ }
+ }
+
+ return _resourceUsages[setIndex];
+ }
+
+ ///
+ /// Gets a resource type from a texture descriptor.
+ ///
+ /// Texture descriptor
+ /// Whether the texture is a image texture (writable) or not (sampled)
+ /// Resource type
private static ResourceType GetTextureResourceType(TextureDescriptor texture, bool isImage)
{
bool isBuffer = (texture.Type & SamplerType.Mask) == SamplerType.TextureBuffer;
@@ -278,10 +340,12 @@ namespace Ryujinx.Graphics.Gpu.Shader
/// Shader information
public ShaderInfo Build(ProgramPipelineState? pipeline, bool fromCache = false)
{
- var descriptors = new ResourceDescriptorCollection[TotalSets];
- var usages = new ResourceUsageCollection[TotalSets];
+ int totalSets = _resourceDescriptors.Length;
- for (int index = 0; index < TotalSets; index++)
+ var descriptors = new ResourceDescriptorCollection[totalSets];
+ var usages = new ResourceUsageCollection[totalSets];
+
+ for (int index = 0; index < totalSets; index++)
{
descriptors[index] = new ResourceDescriptorCollection(_resourceDescriptors[index].ToArray().AsReadOnly());
usages[index] = new ResourceUsageCollection(_resourceUsages[index].ToArray().AsReadOnly());
diff --git a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
index 7bcff947e..ba9cd45c6 100644
--- a/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
+++ b/src/Ryujinx.Graphics.OpenGL/OpenGLRenderer.cs
@@ -187,6 +187,12 @@ namespace Ryujinx.Graphics.OpenGL
supportsViewportSwizzle: HwCapabilities.SupportsViewportSwizzle,
supportsIndirectParameters: HwCapabilities.SupportsIndirectParameters,
supportsDepthClipControl: true,
+ uniformBufferSetIndex: 0,
+ storageBufferSetIndex: 1,
+ textureSetIndex: 2,
+ imageSetIndex: 3,
+ extraSetBaseIndex: 0,
+ maximumExtraSets: 0,
maximumUniformBuffersPerStage: 13, // TODO: Avoid hardcoding those limits here and get from driver?
maximumStorageBuffersPerStage: 16,
maximumTexturesPerStage: 32,
diff --git a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs
index 6d066bb67..54f6b3f7b 100644
--- a/src/Ryujinx.Graphics.OpenGL/Pipeline.cs
+++ b/src/Ryujinx.Graphics.OpenGL/Pipeline.cs
@@ -963,6 +963,11 @@ namespace Ryujinx.Graphics.OpenGL
(array as ImageArray).Bind(binding);
}
+ public void SetImageArraySeparate(ShaderStage stage, int setIndex, IImageArray array)
+ {
+ throw new NotSupportedException("OpenGL does not support descriptor sets.");
+ }
+
public void SetIndexBuffer(BufferRange buffer, IndexType type)
{
_elementsType = type.Convert();
@@ -1312,6 +1317,11 @@ namespace Ryujinx.Graphics.OpenGL
(array as TextureArray).Bind(binding);
}
+ public void SetTextureArraySeparate(ShaderStage stage, int setIndex, ITextureArray array)
+ {
+ throw new NotSupportedException("OpenGL does not support descriptor sets.");
+ }
+
public void SetTransformFeedbackBuffers(ReadOnlySpan buffers)
{
if (_tfEnabled)
diff --git a/src/Ryujinx.Graphics.Shader/BufferDescriptor.cs b/src/Ryujinx.Graphics.Shader/BufferDescriptor.cs
index ead1c5e67..11d4e3c11 100644
--- a/src/Ryujinx.Graphics.Shader/BufferDescriptor.cs
+++ b/src/Ryujinx.Graphics.Shader/BufferDescriptor.cs
@@ -4,14 +4,16 @@ namespace Ryujinx.Graphics.Shader
{
// New fields should be added to the end of the struct to keep disk shader cache compatibility.
+ public readonly int Set;
public readonly int Binding;
public readonly byte Slot;
public readonly byte SbCbSlot;
public readonly ushort SbCbOffset;
public readonly BufferUsageFlags Flags;
- public BufferDescriptor(int binding, int slot)
+ public BufferDescriptor(int set, int binding, int slot)
{
+ Set = set;
Binding = binding;
Slot = (byte)slot;
SbCbSlot = 0;
@@ -19,8 +21,9 @@ namespace Ryujinx.Graphics.Shader
Flags = BufferUsageFlags.None;
}
- public BufferDescriptor(int binding, int slot, int sbCbSlot, int sbCbOffset, BufferUsageFlags flags)
+ public BufferDescriptor(int set, int binding, int slot, int sbCbSlot, int sbCbOffset, BufferUsageFlags flags)
{
+ Set = set;
Binding = binding;
Slot = (byte)slot;
SbCbSlot = (byte)sbCbSlot;
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
index f0e57b534..4308b08f8 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Glsl/Instructions/InstGenMemory.cs
@@ -462,7 +462,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
}
else
{
- context.Properties.Textures.TryGetValue(texOp.Binding, out TextureDefinition definition);
+ context.Properties.Textures.TryGetValue(texOp.GetTextureSetAndBinding(), out TextureDefinition definition);
bool hasLod = !definition.Type.HasFlag(SamplerType.Multisample) && (definition.Type & SamplerType.Mask) != SamplerType.TextureBuffer;
string texCall;
@@ -639,7 +639,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
private static string GetSamplerName(CodeGenContext context, AstTextureOperation texOp, ref int srcIndex)
{
- TextureDefinition textureDefinition = context.Properties.Textures[texOp.Binding];
+ TextureDefinition textureDefinition = context.Properties.Textures[texOp.GetTextureSetAndBinding()];
string name = textureDefinition.Name;
if (textureDefinition.ArrayLength != 1)
@@ -649,7 +649,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
if (texOp.IsSeparate)
{
- TextureDefinition samplerDefinition = context.Properties.Textures[texOp.SamplerBinding];
+ TextureDefinition samplerDefinition = context.Properties.Textures[texOp.GetSamplerSetAndBinding()];
string samplerName = samplerDefinition.Name;
if (samplerDefinition.ArrayLength != 1)
@@ -665,7 +665,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Glsl.Instructions
private static string GetImageName(CodeGenContext context, AstTextureOperation texOp, ref int srcIndex)
{
- TextureDefinition definition = context.Properties.Images[texOp.Binding];
+ TextureDefinition definition = context.Properties.Images[texOp.GetTextureSetAndBinding()];
string name = definition.Name;
if (definition.ArrayLength != 1)
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
index 2b1fdf44c..f3be29bb9 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
@@ -33,9 +33,9 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
public Dictionary LocalMemories { get; } = new();
public Dictionary SharedMemories { get; } = new();
- public Dictionary SamplersTypes { get; } = new();
- public Dictionary Samplers { get; } = new();
- public Dictionary Images { get; } = new();
+ public Dictionary SamplersTypes { get; } = new();
+ public Dictionary Samplers { get; } = new();
+ public Dictionary Images { get; } = new();
public Dictionary Inputs { get; } = new();
public Dictionary Outputs { get; } = new();
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
index 37df4df80..55d35bf0d 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Declarations.cs
@@ -208,13 +208,13 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
var sampledImageVariable = context.Variable(sampledImageArrayPointerType, StorageClass.UniformConstant);
- context.Samplers.Add(sampler.Binding, new SamplerDeclaration(
+ context.Samplers.Add(new(sampler.Set, sampler.Binding), new SamplerDeclaration(
imageType,
sampledImageType,
sampledImagePointerType,
sampledImageVariable,
sampler.ArrayLength != 1));
- context.SamplersTypes.Add(sampler.Binding, sampler.Type);
+ context.SamplersTypes.Add(new(sampler.Set, sampler.Binding), sampler.Type);
context.Name(sampledImageVariable, sampler.Name);
context.Decorate(sampledImageVariable, Decoration.DescriptorSet, (LiteralInteger)setIndex);
@@ -256,7 +256,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
var imageVariable = context.Variable(imageArrayPointerType, StorageClass.UniformConstant);
- context.Images.Add(image.Binding, new ImageDeclaration(imageType, imagePointerType, imageVariable, image.ArrayLength != 1));
+ context.Images.Add(new(image.Set, image.Binding), new ImageDeclaration(imageType, imagePointerType, imageVariable, image.ArrayLength != 1));
context.Name(imageVariable, image.Name);
context.Decorate(imageVariable, Decoration.DescriptorSet, (LiteralInteger)setIndex);
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
index 34f8532a6..6206985d8 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/Instructions.cs
@@ -602,7 +602,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
return context.Get(type, texOp.GetSource(srcIndex++));
}
- ImageDeclaration declaration = context.Images[texOp.Binding];
+ ImageDeclaration declaration = context.Images[texOp.GetTextureSetAndBinding()];
SpvInstruction image = declaration.Image;
SpvInstruction resultType = context.GetType(componentType);
@@ -681,7 +681,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
return context.Get(type, texOp.GetSource(srcIndex++));
}
- ImageDeclaration declaration = context.Images[texOp.Binding];
+ ImageDeclaration declaration = context.Images[texOp.GetTextureSetAndBinding()];
SpvInstruction image = declaration.Image;
if (declaration.IsIndexed)
@@ -738,7 +738,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
return context.Get(type, texOp.GetSource(srcIndex++));
}
- ImageDeclaration declaration = context.Images[texOp.Binding];
+ ImageDeclaration declaration = context.Images[texOp.GetTextureSetAndBinding()];
SpvInstruction image = declaration.Image;
if (declaration.IsIndexed)
@@ -837,7 +837,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
return context.Get(type, texOp.GetSource(srcIndex++));
}
- SamplerDeclaration declaration = context.Samplers[texOp.Binding];
+ SamplerDeclaration declaration = context.Samplers[texOp.GetTextureSetAndBinding()];
SpvInstruction image = GenerateSampledImageLoad(context, texOp, declaration, ref srcIndex);
int pCount = texOp.Type.GetDimensions();
@@ -1161,7 +1161,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
return context.Get(type, texOp.GetSource(srcIndex++));
}
- SamplerDeclaration declaration = context.Samplers[texOp.Binding];
+ SamplerDeclaration declaration = context.Samplers[texOp.GetTextureSetAndBinding()];
SpvInstruction image = GenerateSampledImageLoad(context, texOp, declaration, ref srcIndex);
int coordsCount = texOp.Type.GetDimensions();
@@ -1433,7 +1433,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
int srcIndex = 0;
- SamplerDeclaration declaration = context.Samplers[texOp.Binding];
+ SamplerDeclaration declaration = context.Samplers[texOp.GetTextureSetAndBinding()];
SpvInstruction image = GenerateSampledImageLoad(context, texOp, declaration, ref srcIndex);
image = context.Image(declaration.ImageType, image);
@@ -1449,7 +1449,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
int srcIndex = 0;
- SamplerDeclaration declaration = context.Samplers[texOp.Binding];
+ SamplerDeclaration declaration = context.Samplers[texOp.GetTextureSetAndBinding()];
SpvInstruction image = GenerateSampledImageLoad(context, texOp, declaration, ref srcIndex);
image = context.Image(declaration.ImageType, image);
@@ -1460,7 +1460,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
}
else
{
- var type = context.SamplersTypes[texOp.Binding];
+ var type = context.SamplersTypes[texOp.GetTextureSetAndBinding()];
bool hasLod = !type.HasFlag(SamplerType.Multisample) && type != SamplerType.TextureBuffer;
int dimensions = (type & SamplerType.Mask) == SamplerType.TextureCube ? 2 : type.GetDimensions();
@@ -1889,7 +1889,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
{
image = context.Load(declaration.ImageType, image);
- SamplerDeclaration samplerDeclaration = context.Samplers[texOp.SamplerBinding];
+ SamplerDeclaration samplerDeclaration = context.Samplers[texOp.GetSamplerSetAndBinding()];
SpvInstruction sampler = samplerDeclaration.Image;
diff --git a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs
index 3dc4ad907..4e6d6edf9 100644
--- a/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs
+++ b/src/Ryujinx.Graphics.Shader/IGpuAccessor.cs
@@ -27,34 +27,43 @@ namespace Ryujinx.Graphics.Shader
ReadOnlySpan GetCode(ulong address, int minimumSize);
///
- /// Queries the binding number of a constant buffer.
+ /// Gets the binding number of a constant buffer.
///
/// Constant buffer index
/// Binding number
- int CreateConstantBufferBinding(int index);
+ SetBindingPair CreateConstantBufferBinding(int index);
///
- /// Queries the binding number of an image.
+ /// Gets the binding number of an image.
///
/// For array of images, the number of elements of the array, otherwise it should be 1
/// Indicates if the image is a buffer image
/// Binding number
- int CreateImageBinding(int count, bool isBuffer);
+ SetBindingPair CreateImageBinding(int count, bool isBuffer);
///
- /// Queries the binding number of a storage buffer.
+ /// Gets the binding number of a storage buffer.
///
/// Storage buffer index
/// Binding number
- int CreateStorageBufferBinding(int index);
+ SetBindingPair CreateStorageBufferBinding(int index);
///
- /// Queries the binding number of a texture.
+ /// Gets the binding number of a texture.
///
/// For array of textures, the number of elements of the array, otherwise it should be 1
/// Indicates if the texture is a buffer texture
/// Binding number
- int CreateTextureBinding(int count, bool isBuffer);
+ SetBindingPair CreateTextureBinding(int count, bool isBuffer);
+
+ ///
+ /// Gets the set index for an additional set, or -1 if there's no extra set available.
+ ///
+ /// Extra set index, or -1 if not available
+ int CreateExtraSet()
+ {
+ return -1;
+ }
///
/// Queries Local Size X for compute shaders.
diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitSurface.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitSurface.cs
index 0aac0ffa8..383e82c69 100644
--- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitSurface.cs
+++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitSurface.cs
@@ -278,7 +278,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
flags |= TextureFlags.Bindless;
}
- int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding(
+ SetBindingPair setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding(
Instruction.ImageAtomic,
type,
format,
@@ -286,7 +286,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
TextureOperation.DefaultCbufSlot,
imm);
- Operand res = context.ImageAtomic(type, format, flags, binding, sources);
+ Operand res = context.ImageAtomic(type, format, flags, setAndBinding, sources);
context.Copy(d, res);
}
@@ -389,7 +389,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
TextureFormat format = isBindless ? TextureFormat.Unknown : ShaderProperties.GetTextureFormat(context.TranslatorContext.GpuAccessor, handle);
- int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding(
+ SetBindingPair setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding(
Instruction.ImageLoad,
type,
format,
@@ -397,7 +397,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
TextureOperation.DefaultCbufSlot,
handle);
- context.ImageLoad(type, format, flags, binding, (int)componentMask, dests, sources);
+ context.ImageLoad(type, format, flags, setAndBinding, (int)componentMask, dests, sources);
}
else
{
@@ -432,7 +432,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
TextureFormat format = GetTextureFormat(size);
- int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding(
+ SetBindingPair setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding(
Instruction.ImageLoad,
type,
format,
@@ -440,7 +440,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
TextureOperation.DefaultCbufSlot,
handle);
- context.ImageLoad(type, format, flags, binding, compMask, dests, sources);
+ context.ImageLoad(type, format, flags, setAndBinding, compMask, dests, sources);
switch (size)
{
@@ -552,7 +552,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
flags |= TextureFlags.Bindless;
}
- int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding(
+ SetBindingPair setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding(
Instruction.ImageAtomic,
type,
format,
@@ -560,7 +560,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
TextureOperation.DefaultCbufSlot,
imm);
- context.ImageAtomic(type, format, flags, binding, sources);
+ context.ImageAtomic(type, format, flags, setAndBinding, sources);
}
private static void EmitSust(
@@ -679,7 +679,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
flags |= TextureFlags.Coherent;
}
- int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding(
+ SetBindingPair setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding(
Instruction.ImageStore,
type,
format,
@@ -687,7 +687,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
TextureOperation.DefaultCbufSlot,
handle);
- context.ImageStore(type, format, flags, binding, sources);
+ context.ImageStore(type, format, flags, setAndBinding, sources);
}
private static int GetComponentSizeInBytesLog2(SuatomSize size)
diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs
index 06daa26a0..2076262da 100644
--- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs
+++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitTexture.cs
@@ -885,7 +885,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
return Register(dest++, RegisterType.Gpr);
}
- int binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding(
+ SetBindingPair setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding(
Instruction.Lod,
type,
TextureFormat.Unknown,
@@ -913,7 +913,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
else
{
// The instruction component order is the inverse of GLSL's.
- Operand res = context.Lod(type, flags, binding, compIndex ^ 1, sources);
+ Operand res = context.Lod(type, flags, setAndBinding, compIndex ^ 1, sources);
res = context.FPMultiply(res, ConstF(256.0f));
@@ -1116,12 +1116,12 @@ namespace Ryujinx.Graphics.Shader.Instructions
}
TextureFlags flags = isBindless ? TextureFlags.Bindless : TextureFlags.None;
- int binding;
+ SetBindingPair setAndBinding;
switch (query)
{
case TexQuery.TexHeaderDimension:
- binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding(
+ setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding(
Instruction.TextureQuerySize,
type,
TextureFormat.Unknown,
@@ -1140,13 +1140,13 @@ namespace Ryujinx.Graphics.Shader.Instructions
break;
}
- context.Copy(d, context.TextureQuerySize(type, flags, binding, compIndex, sources));
+ context.Copy(d, context.TextureQuerySize(type, flags, setAndBinding, compIndex, sources));
}
}
break;
case TexQuery.TexHeaderTextureType:
- binding = isBindless ? 0 : context.ResourceManager.GetTextureOrImageBinding(
+ setAndBinding = isBindless ? default : context.ResourceManager.GetTextureOrImageBinding(
Instruction.TextureQuerySamples,
type,
TextureFormat.Unknown,
@@ -1171,7 +1171,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
if (d != null)
{
- context.Copy(d, context.TextureQuerySamples(type, flags, binding, sources));
+ context.Copy(d, context.TextureQuerySamples(type, flags, setAndBinding, sources));
}
}
break;
@@ -1191,7 +1191,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
Operand[] dests,
Operand[] sources)
{
- int binding = flags.HasFlag(TextureFlags.Bindless) ? 0 : context.ResourceManager.GetTextureOrImageBinding(
+ SetBindingPair setAndBinding = flags.HasFlag(TextureFlags.Bindless) ? default : context.ResourceManager.GetTextureOrImageBinding(
Instruction.TextureSample,
type,
TextureFormat.Unknown,
@@ -1199,7 +1199,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
TextureOperation.DefaultCbufSlot,
handle);
- context.TextureSample(type, flags, binding, componentMask, dests, sources);
+ context.TextureSample(type, flags, setAndBinding, componentMask, dests, sources);
}
private static SamplerType ConvertSamplerType(TexDim dimensions)
diff --git a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs
index 74ec5ca61..7eee8f2e9 100644
--- a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs
+++ b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/TextureOperation.cs
@@ -8,7 +8,9 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
public TextureFormat Format { get; set; }
public TextureFlags Flags { get; private set; }
+ public int Set { get; private set; }
public int Binding { get; private set; }
+ public int SamplerSet { get; private set; }
public int SamplerBinding { get; private set; }
public TextureOperation(
@@ -16,6 +18,7 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
SamplerType type,
TextureFormat format,
TextureFlags flags,
+ int set,
int binding,
int compIndex,
Operand[] dests,
@@ -24,24 +27,28 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
Type = type;
Format = format;
Flags = flags;
+ Set = set;
Binding = binding;
+ SamplerSet = -1;
SamplerBinding = -1;
}
- public void TurnIntoArray(int binding)
+ public void TurnIntoArray(SetBindingPair setAndBinding)
{
Flags &= ~TextureFlags.Bindless;
- Binding = binding;
+ Set = setAndBinding.SetIndex;
+ Binding = setAndBinding.Binding;
}
- public void TurnIntoArray(int textureBinding, int samplerBinding)
+ public void TurnIntoArray(SetBindingPair textureSetAndBinding, SetBindingPair samplerSetAndBinding)
{
- TurnIntoArray(textureBinding);
+ TurnIntoArray(textureSetAndBinding);
- SamplerBinding = samplerBinding;
+ SamplerSet = samplerSetAndBinding.SetIndex;
+ SamplerBinding = samplerSetAndBinding.Binding;
}
- public void SetBinding(int binding)
+ public void SetBinding(SetBindingPair setAndBinding)
{
if ((Flags & TextureFlags.Bindless) != 0)
{
@@ -50,7 +57,8 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
RemoveSource(0);
}
- Binding = binding;
+ Set = setAndBinding.SetIndex;
+ Binding = setAndBinding.Binding;
}
public void SetLodLevelFlag()
diff --git a/src/Ryujinx.Graphics.Shader/SetBindingPair.cs b/src/Ryujinx.Graphics.Shader/SetBindingPair.cs
new file mode 100644
index 000000000..1e8a4f9c6
--- /dev/null
+++ b/src/Ryujinx.Graphics.Shader/SetBindingPair.cs
@@ -0,0 +1,4 @@
+namespace Ryujinx.Graphics.Shader
+{
+ public readonly record struct SetBindingPair(int SetIndex, int Binding);
+}
diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs
index 4068c4127..867cae853 100644
--- a/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs
+++ b/src/Ryujinx.Graphics.Shader/StructuredIr/AstTextureOperation.cs
@@ -8,7 +8,9 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
public TextureFormat Format { get; }
public TextureFlags Flags { get; }
+ public int Set { get; }
public int Binding { get; }
+ public int SamplerSet { get; }
public int SamplerBinding { get; }
public bool IsSeparate => SamplerBinding >= 0;
@@ -18,7 +20,9 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
SamplerType type,
TextureFormat format,
TextureFlags flags,
+ int set,
int binding,
+ int samplerSet,
int samplerBinding,
int index,
params IAstNode[] sources) : base(inst, StorageKind.None, false, index, sources, sources.Length)
@@ -26,8 +30,20 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
Type = type;
Format = format;
Flags = flags;
+ Set = set;
Binding = binding;
+ SamplerSet = samplerSet;
SamplerBinding = samplerBinding;
}
+
+ public SetBindingPair GetTextureSetAndBinding()
+ {
+ return new SetBindingPair(Set, Binding);
+ }
+
+ public SetBindingPair GetSamplerSetAndBinding()
+ {
+ return new SetBindingPair(SamplerSet, SamplerBinding);
+ }
}
}
diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs
index 8c12c2aaf..53ed6bfcc 100644
--- a/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs
+++ b/src/Ryujinx.Graphics.Shader/StructuredIr/ShaderProperties.cs
@@ -6,15 +6,15 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
{
private readonly Dictionary _constantBuffers;
private readonly Dictionary _storageBuffers;
- private readonly Dictionary _textures;
- private readonly Dictionary _images;
+ private readonly Dictionary _textures;
+ private readonly Dictionary _images;
private readonly Dictionary _localMemories;
private readonly Dictionary _sharedMemories;
public IReadOnlyDictionary ConstantBuffers => _constantBuffers;
public IReadOnlyDictionary StorageBuffers => _storageBuffers;
- public IReadOnlyDictionary Textures => _textures;
- public IReadOnlyDictionary Images => _images;
+ public IReadOnlyDictionary Textures => _textures;
+ public IReadOnlyDictionary Images => _images;
public IReadOnlyDictionary LocalMemories => _localMemories;
public IReadOnlyDictionary SharedMemories => _sharedMemories;
@@ -22,8 +22,8 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
{
_constantBuffers = new Dictionary();
_storageBuffers = new Dictionary();
- _textures = new Dictionary();
- _images = new Dictionary();
+ _textures = new Dictionary();
+ _images = new Dictionary();
_localMemories = new Dictionary();
_sharedMemories = new Dictionary();
}
@@ -40,12 +40,12 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
public void AddOrUpdateTexture(TextureDefinition definition)
{
- _textures[definition.Binding] = definition;
+ _textures[new(definition.Set, definition.Binding)] = definition;
}
public void AddOrUpdateImage(TextureDefinition definition)
{
- _images[definition.Binding] = definition;
+ _images[new(definition.Set, definition.Binding)] = definition;
}
public int AddLocalMemory(MemoryDefinition definition)
diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs
index c4ebaee73..88053658d 100644
--- a/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs
+++ b/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs
@@ -169,7 +169,17 @@ namespace Ryujinx.Graphics.Shader.StructuredIr
AstTextureOperation GetAstTextureOperation(TextureOperation texOp)
{
- return new AstTextureOperation(inst, texOp.Type, texOp.Format, texOp.Flags, texOp.Binding, texOp.SamplerBinding, texOp.Index, sources);
+ return new AstTextureOperation(
+ inst,
+ texOp.Type,
+ texOp.Format,
+ texOp.Flags,
+ texOp.Set,
+ texOp.Binding,
+ texOp.SamplerSet,
+ texOp.SamplerBinding,
+ texOp.Index,
+ sources);
}
int componentsCount = BitOperations.PopCount((uint)operation.Index);
diff --git a/src/Ryujinx.Graphics.Shader/TextureDescriptor.cs b/src/Ryujinx.Graphics.Shader/TextureDescriptor.cs
index d287a1aa7..1e387407d 100644
--- a/src/Ryujinx.Graphics.Shader/TextureDescriptor.cs
+++ b/src/Ryujinx.Graphics.Shader/TextureDescriptor.cs
@@ -4,6 +4,7 @@ namespace Ryujinx.Graphics.Shader
{
// New fields should be added to the end of the struct to keep disk shader cache compatibility.
+ public readonly int Set;
public readonly int Binding;
public readonly SamplerType Type;
@@ -18,6 +19,7 @@ namespace Ryujinx.Graphics.Shader
public readonly TextureUsageFlags Flags;
public TextureDescriptor(
+ int set,
int binding,
SamplerType type,
TextureFormat format,
@@ -27,6 +29,7 @@ namespace Ryujinx.Graphics.Shader
bool separate,
TextureUsageFlags flags)
{
+ Set = set;
Binding = binding;
Type = type;
Format = format;
diff --git a/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs b/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
index e1157eea4..5e07b39f1 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/EmitterContext.cs
@@ -124,7 +124,7 @@ namespace Ryujinx.Graphics.Shader.Translation
this.TextureSample(
SamplerType.TextureBuffer,
TextureFlags.IntCoords,
- ResourceManager.Reservations.IndexBufferTextureBinding,
+ ResourceManager.Reservations.GetIndexBufferTextureSetAndBinding(),
1,
new[] { vertexIndexVr },
new[] { this.IAdd(ibBaseOffset, outputVertexOffset) });
@@ -145,7 +145,7 @@ namespace Ryujinx.Graphics.Shader.Translation
this.TextureSample(
SamplerType.TextureBuffer,
TextureFlags.IntCoords,
- ResourceManager.Reservations.TopologyRemapBufferTextureBinding,
+ ResourceManager.Reservations.GetTopologyRemapBufferTextureSetAndBinding(),
1,
new[] { vertexIndex },
new[] { this.IAdd(baseVertex, Const(index)) });
diff --git a/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs b/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs
index 9e314c620..5bdbb0025 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/EmitterContextInsts.cs
@@ -618,12 +618,21 @@ namespace Ryujinx.Graphics.Shader.Translation
SamplerType type,
TextureFormat format,
TextureFlags flags,
- int binding,
+ SetBindingPair setAndBinding,
Operand[] sources)
{
Operand dest = Local();
- context.Add(new TextureOperation(Instruction.ImageAtomic, type, format, flags, binding, 0, new[] { dest }, sources));
+ context.Add(new TextureOperation(
+ Instruction.ImageAtomic,
+ type,
+ format,
+ flags,
+ setAndBinding.SetIndex,
+ setAndBinding.Binding,
+ 0,
+ new[] { dest },
+ sources));
return dest;
}
@@ -633,12 +642,21 @@ namespace Ryujinx.Graphics.Shader.Translation
SamplerType type,
TextureFormat format,
TextureFlags flags,
- int binding,
+ SetBindingPair setAndBinding,
int compMask,
Operand[] dests,
Operand[] sources)
{
- context.Add(new TextureOperation(Instruction.ImageLoad, type, format, flags, binding, compMask, dests, sources));
+ context.Add(new TextureOperation(
+ Instruction.ImageLoad,
+ type,
+ format,
+ flags,
+ setAndBinding.SetIndex,
+ setAndBinding.Binding,
+ compMask,
+ dests,
+ sources));
}
public static void ImageStore(
@@ -646,10 +664,19 @@ namespace Ryujinx.Graphics.Shader.Translation
SamplerType type,
TextureFormat format,
TextureFlags flags,
- int binding,
+ SetBindingPair setAndBinding,
Operand[] sources)
{
- context.Add(new TextureOperation(Instruction.ImageStore, type, format, flags, binding, 0, null, sources));
+ context.Add(new TextureOperation(
+ Instruction.ImageStore,
+ type,
+ format,
+ flags,
+ setAndBinding.SetIndex,
+ setAndBinding.Binding,
+ 0,
+ null,
+ sources));
}
public static Operand IsNan(this EmitterContext context, Operand a, Instruction fpType = Instruction.FP32)
@@ -718,13 +745,22 @@ namespace Ryujinx.Graphics.Shader.Translation
this EmitterContext context,
SamplerType type,
TextureFlags flags,
- int binding,
+ SetBindingPair setAndBinding,
int compIndex,
Operand[] sources)
{
Operand dest = Local();
- context.Add(new TextureOperation(Instruction.Lod, type, TextureFormat.Unknown, flags, binding, compIndex, new[] { dest }, sources));
+ context.Add(new TextureOperation(
+ Instruction.Lod,
+ type,
+ TextureFormat.Unknown,
+ flags,
+ setAndBinding.SetIndex,
+ setAndBinding.Binding,
+ compIndex,
+ new[] { dest },
+ sources));
return dest;
}
@@ -889,24 +925,42 @@ namespace Ryujinx.Graphics.Shader.Translation
this EmitterContext context,
SamplerType type,
TextureFlags flags,
- int binding,
+ SetBindingPair setAndBinding,
int compMask,
Operand[] dests,
Operand[] sources)
{
- context.Add(new TextureOperation(Instruction.TextureSample, type, TextureFormat.Unknown, flags, binding, compMask, dests, sources));
+ context.Add(new TextureOperation(
+ Instruction.TextureSample,
+ type,
+ TextureFormat.Unknown,
+ flags,
+ setAndBinding.SetIndex,
+ setAndBinding.Binding,
+ compMask,
+ dests,
+ sources));
}
public static Operand TextureQuerySamples(
this EmitterContext context,
SamplerType type,
TextureFlags flags,
- int binding,
+ SetBindingPair setAndBinding,
Operand[] sources)
{
Operand dest = Local();
- context.Add(new TextureOperation(Instruction.TextureQuerySamples, type, TextureFormat.Unknown, flags, binding, 0, new[] { dest }, sources));
+ context.Add(new TextureOperation(
+ Instruction.TextureQuerySamples,
+ type,
+ TextureFormat.Unknown,
+ flags,
+ setAndBinding.SetIndex,
+ setAndBinding.Binding,
+ 0,
+ new[] { dest },
+ sources));
return dest;
}
@@ -915,13 +969,22 @@ namespace Ryujinx.Graphics.Shader.Translation
this EmitterContext context,
SamplerType type,
TextureFlags flags,
- int binding,
+ SetBindingPair setAndBinding,
int compIndex,
Operand[] sources)
{
Operand dest = Local();
- context.Add(new TextureOperation(Instruction.TextureQuerySize, type, TextureFormat.Unknown, flags, binding, compIndex, new[] { dest }, sources));
+ context.Add(new TextureOperation(
+ Instruction.TextureQuerySize,
+ type,
+ TextureFormat.Unknown,
+ flags,
+ setAndBinding.SetIndex,
+ setAndBinding.Binding,
+ compIndex,
+ new[] { dest },
+ sources));
return dest;
}
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
index 4128af241..29501b710 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
@@ -91,7 +91,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
bool hasSampler = !texOp.Inst.IsImage();
- int textureBinding = resourceManager.GetTextureOrImageBinding(
+ SetBindingPair textureSetAndBinding = resourceManager.GetTextureOrImageBinding(
texOp.Inst,
texOp.Type,
texOp.Format,
@@ -111,7 +111,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
texOp.InsertSource(1, samplerIndex);
- int samplerBinding = resourceManager.GetTextureOrImageBinding(
+ SetBindingPair samplerSetAndBinding = resourceManager.GetTextureOrImageBinding(
texOp.Inst,
SamplerType.None,
texOp.Format,
@@ -120,11 +120,11 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
TextureHandle.PackOffsets(0, 0, TextureHandleType.Direct),
samplerPoolLength);
- texOp.TurnIntoArray(textureBinding, samplerBinding);
+ texOp.TurnIntoArray(textureSetAndBinding, samplerSetAndBinding);
}
else
{
- texOp.TurnIntoArray(textureBinding);
+ texOp.TurnIntoArray(textureSetAndBinding);
}
return true;
@@ -445,7 +445,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
}
}
- int binding = resourceManager.GetTextureOrImageBinding(
+ SetBindingPair setAndBinding = resourceManager.GetTextureOrImageBinding(
texOp.Inst,
texOp.Type,
texOp.Format,
@@ -453,7 +453,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
cbufSlot,
cbufOffset);
- texOp.SetBinding(binding);
+ texOp.SetBinding(setAndBinding);
}
}
}
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToArray.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToArray.cs
index f2be7975d..8eed139d6 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToArray.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToArray.cs
@@ -221,7 +221,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
private static void TurnIntoArray(ResourceManager resourceManager, TextureOperation texOp, int cbufSlot, int handleIndex, int length)
{
- int binding = resourceManager.GetTextureOrImageBinding(
+ SetBindingPair setAndBinding = resourceManager.GetTextureOrImageBinding(
texOp.Inst,
texOp.Type,
texOp.Format,
@@ -230,7 +230,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
handleIndex,
length);
- texOp.TurnIntoArray(binding);
+ texOp.TurnIntoArray(setAndBinding);
}
}
}
diff --git a/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs b/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs
index 890501c91..94691a5b4 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs
@@ -20,8 +20,8 @@ namespace Ryujinx.Graphics.Shader.Translation
private readonly ShaderStage _stage;
private readonly string _stagePrefix;
- private readonly int[] _cbSlotToBindingMap;
- private readonly int[] _sbSlotToBindingMap;
+ private readonly SetBindingPair[] _cbSlotToBindingMap;
+ private readonly SetBindingPair[] _sbSlotToBindingMap;
private uint _sbSlotWritten;
private readonly Dictionary _sbSlots;
@@ -33,6 +33,7 @@ namespace Ryujinx.Graphics.Shader.Translation
private struct TextureMeta
{
+ public int Set;
public int Binding;
public bool AccurateType;
public SamplerType Type;
@@ -64,10 +65,10 @@ namespace Ryujinx.Graphics.Shader.Translation
_stage = stage;
_stagePrefix = GetShaderStagePrefix(stage);
- _cbSlotToBindingMap = new int[18];
- _sbSlotToBindingMap = new int[16];
- _cbSlotToBindingMap.AsSpan().Fill(-1);
- _sbSlotToBindingMap.AsSpan().Fill(-1);
+ _cbSlotToBindingMap = new SetBindingPair[18];
+ _sbSlotToBindingMap = new SetBindingPair[16];
+ _cbSlotToBindingMap.AsSpan().Fill(new(-1, -1));
+ _sbSlotToBindingMap.AsSpan().Fill(new(-1, -1));
_sbSlots = new();
_sbSlotsReverse = new();
@@ -146,16 +147,16 @@ namespace Ryujinx.Graphics.Shader.Translation
public int GetConstantBufferBinding(int slot)
{
- int binding = _cbSlotToBindingMap[slot];
- if (binding < 0)
+ SetBindingPair setAndBinding = _cbSlotToBindingMap[slot];
+ if (setAndBinding.Binding < 0)
{
- binding = _gpuAccessor.CreateConstantBufferBinding(slot);
- _cbSlotToBindingMap[slot] = binding;
+ setAndBinding = _gpuAccessor.CreateConstantBufferBinding(slot);
+ _cbSlotToBindingMap[slot] = setAndBinding;
string slotNumber = slot.ToString(CultureInfo.InvariantCulture);
- AddNewConstantBuffer(binding, $"{_stagePrefix}_c{slotNumber}");
+ AddNewConstantBuffer(setAndBinding.SetIndex, setAndBinding.Binding, $"{_stagePrefix}_c{slotNumber}");
}
- return binding;
+ return setAndBinding.Binding;
}
public bool TryGetStorageBufferBinding(int sbCbSlot, int sbCbOffset, bool write, out int binding)
@@ -166,14 +167,14 @@ namespace Ryujinx.Graphics.Shader.Translation
return false;
}
- binding = _sbSlotToBindingMap[slot];
+ SetBindingPair setAndBinding = _sbSlotToBindingMap[slot];
- if (binding < 0)
+ if (setAndBinding.Binding < 0)
{
- binding = _gpuAccessor.CreateStorageBufferBinding(slot);
- _sbSlotToBindingMap[slot] = binding;
+ setAndBinding = _gpuAccessor.CreateStorageBufferBinding(slot);
+ _sbSlotToBindingMap[slot] = setAndBinding;
string slotNumber = slot.ToString(CultureInfo.InvariantCulture);
- AddNewStorageBuffer(binding, $"{_stagePrefix}_s{slotNumber}");
+ AddNewStorageBuffer(setAndBinding.SetIndex, setAndBinding.Binding, $"{_stagePrefix}_s{slotNumber}");
}
if (write)
@@ -181,6 +182,7 @@ namespace Ryujinx.Graphics.Shader.Translation
_sbSlotWritten |= 1u << slot;
}
+ binding = setAndBinding.Binding;
return true;
}
@@ -208,7 +210,7 @@ namespace Ryujinx.Graphics.Shader.Translation
{
for (slot = 0; slot < _cbSlotToBindingMap.Length; slot++)
{
- if (_cbSlotToBindingMap[slot] == binding)
+ if (_cbSlotToBindingMap[slot].Binding == binding)
{
return true;
}
@@ -218,7 +220,7 @@ namespace Ryujinx.Graphics.Shader.Translation
return false;
}
- public int GetTextureOrImageBinding(
+ public SetBindingPair GetTextureOrImageBinding(
Instruction inst,
SamplerType type,
TextureFormat format,
@@ -240,7 +242,7 @@ namespace Ryujinx.Graphics.Shader.Translation
format = TextureFormat.Unknown;
}
- int binding = GetTextureOrImageBinding(
+ SetBindingPair setAndBinding = GetTextureOrImageBinding(
cbufSlot,
handle,
arrayLength,
@@ -255,10 +257,10 @@ namespace Ryujinx.Graphics.Shader.Translation
_gpuAccessor.RegisterTexture(handle, cbufSlot);
- return binding;
+ return setAndBinding;
}
- private int GetTextureOrImageBinding(
+ private SetBindingPair GetTextureOrImageBinding(
int cbufSlot,
int handle,
int arrayLength,
@@ -311,21 +313,38 @@ namespace Ryujinx.Graphics.Shader.Translation
UsageFlags = usageFlags,
};
+ int setIndex;
int binding;
if (dict.TryGetValue(info, out var existingMeta))
{
dict[info] = MergeTextureMeta(meta, existingMeta);
+ setIndex = existingMeta.Set;
binding = existingMeta.Binding;
}
else
{
- bool isBuffer = (type & SamplerType.Mask) == SamplerType.TextureBuffer;
+ if (arrayLength > 1 && (setIndex = _gpuAccessor.CreateExtraSet()) >= 0)
+ {
+ // We reserved an "extra set" for the array.
+ // In this case the binding is always the first one (0).
+ // Using separate sets for array is better as we need to do less descriptor set updates.
- binding = isImage
- ? _gpuAccessor.CreateImageBinding(arrayLength, isBuffer)
- : _gpuAccessor.CreateTextureBinding(arrayLength, isBuffer);
+ binding = 0;
+ }
+ else
+ {
+ bool isBuffer = (type & SamplerType.Mask) == SamplerType.TextureBuffer;
+ SetBindingPair setAndBinding = isImage
+ ? _gpuAccessor.CreateImageBinding(arrayLength, isBuffer)
+ : _gpuAccessor.CreateTextureBinding(arrayLength, isBuffer);
+
+ setIndex = setAndBinding.SetIndex;
+ binding = setAndBinding.Binding;
+ }
+
+ meta.Set = setIndex;
meta.Binding = binding;
dict.Add(info, meta);
@@ -355,7 +374,7 @@ namespace Ryujinx.Graphics.Shader.Translation
}
var definition = new TextureDefinition(
- isImage ? 3 : 2,
+ setIndex,
binding,
arrayLength,
separate,
@@ -373,11 +392,12 @@ namespace Ryujinx.Graphics.Shader.Translation
Properties.AddOrUpdateTexture(definition);
}
- return binding;
+ return new SetBindingPair(setIndex, binding);
}
private static TextureMeta MergeTextureMeta(TextureMeta meta, TextureMeta existingMeta)
{
+ meta.Set = existingMeta.Set;
meta.Binding = existingMeta.Binding;
meta.UsageFlags |= existingMeta.UsageFlags;
@@ -440,11 +460,11 @@ namespace Ryujinx.Graphics.Shader.Translation
for (int slot = 0; slot < _cbSlotToBindingMap.Length; slot++)
{
- int binding = _cbSlotToBindingMap[slot];
+ SetBindingPair setAndBinding = _cbSlotToBindingMap[slot];
- if (binding >= 0 && _usedConstantBufferBindings.Contains(binding))
+ if (setAndBinding.Binding >= 0 && _usedConstantBufferBindings.Contains(setAndBinding.Binding))
{
- descriptors[descriptorIndex++] = new BufferDescriptor(binding, slot);
+ descriptors[descriptorIndex++] = new BufferDescriptor(setAndBinding.SetIndex, setAndBinding.Binding, slot);
}
}
@@ -464,13 +484,13 @@ namespace Ryujinx.Graphics.Shader.Translation
foreach ((int key, int slot) in _sbSlots)
{
- int binding = _sbSlotToBindingMap[slot];
+ SetBindingPair setAndBinding = _sbSlotToBindingMap[slot];
- if (binding >= 0)
+ if (setAndBinding.Binding >= 0)
{
(int sbCbSlot, int sbCbOffset) = UnpackSbCbInfo(key);
BufferUsageFlags flags = (_sbSlotWritten & (1u << slot)) != 0 ? BufferUsageFlags.Write : BufferUsageFlags.None;
- descriptors[descriptorIndex++] = new BufferDescriptor(binding, slot, sbCbSlot, sbCbOffset, flags);
+ descriptors[descriptorIndex++] = new BufferDescriptor(setAndBinding.SetIndex, setAndBinding.Binding, slot, sbCbSlot, sbCbOffset, flags);
}
}
@@ -507,6 +527,7 @@ namespace Ryujinx.Graphics.Shader.Translation
}
descriptors.Add(new TextureDescriptor(
+ meta.Set,
meta.Binding,
meta.Type,
info.Format,
@@ -527,6 +548,7 @@ namespace Ryujinx.Graphics.Shader.Translation
}
descriptors.Add(new TextureDescriptor(
+ meta.Set,
meta.Binding,
meta.Type,
info.Format,
@@ -587,24 +609,24 @@ namespace Ryujinx.Graphics.Shader.Translation
return false;
}
- private void AddNewConstantBuffer(int binding, string name)
+ private void AddNewConstantBuffer(int setIndex, int binding, string name)
{
StructureType type = new(new[]
{
new StructureField(AggregateType.Array | AggregateType.Vector4 | AggregateType.FP32, "data", Constants.ConstantBufferSize / 16),
});
- Properties.AddOrUpdateConstantBuffer(new(BufferLayout.Std140, 0, binding, name, type));
+ Properties.AddOrUpdateConstantBuffer(new(BufferLayout.Std140, setIndex, binding, name, type));
}
- private void AddNewStorageBuffer(int binding, string name)
+ private void AddNewStorageBuffer(int setIndex, int binding, string name)
{
StructureType type = new(new[]
{
new StructureField(AggregateType.Array | AggregateType.U32, "data", 0),
});
- Properties.AddOrUpdateStorageBuffer(new(BufferLayout.Std430, 1, binding, name, type));
+ Properties.AddOrUpdateStorageBuffer(new(BufferLayout.Std430, setIndex, binding, name, type));
}
public static string GetShaderStagePrefix(ShaderStage stage)
diff --git a/src/Ryujinx.Graphics.Shader/Translation/ResourceReservations.cs b/src/Ryujinx.Graphics.Shader/Translation/ResourceReservations.cs
index d559f6699..c89c4d0b6 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/ResourceReservations.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/ResourceReservations.cs
@@ -11,6 +11,8 @@ namespace Ryujinx.Graphics.Shader.Translation
public const int MaxVertexBufferTextures = 32;
+ private const int TextureSetIndex = 2; // TODO: Get from GPU accessor.
+
public int VertexInfoConstantBufferBinding { get; }
public int VertexOutputStorageBufferBinding { get; }
public int GeometryVertexOutputStorageBufferBinding { get; }
@@ -163,6 +165,21 @@ namespace Ryujinx.Graphics.Shader.Translation
return _vertexBufferTextureBaseBinding + vaLocation;
}
+ public SetBindingPair GetVertexBufferTextureSetAndBinding(int vaLocation)
+ {
+ return new SetBindingPair(TextureSetIndex, GetVertexBufferTextureBinding(vaLocation));
+ }
+
+ public SetBindingPair GetIndexBufferTextureSetAndBinding()
+ {
+ return new SetBindingPair(TextureSetIndex, IndexBufferTextureBinding);
+ }
+
+ public SetBindingPair GetTopologyRemapBufferTextureSetAndBinding()
+ {
+ return new SetBindingPair(TextureSetIndex, TopologyRemapBufferTextureBinding);
+ }
+
internal bool TryGetOffset(StorageKind storageKind, int location, int component, out int offset)
{
return _offsets.TryGetValue(new IoDefinition(storageKind, IoVariable.UserDefined, location, component), out offset);
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs b/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs
index 072b45695..6ba8cb44a 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Transforms/TexturePass.cs
@@ -182,6 +182,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
texOp.Type,
texOp.Format,
texOp.Flags,
+ texOp.Set,
texOp.Binding,
index,
new[] { coordSize },
@@ -251,6 +252,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
texOp.Type,
texOp.Format,
texOp.Flags,
+ texOp.Set,
texOp.Binding,
index,
new[] { coordSize },
@@ -471,6 +473,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
texOp.Type,
texOp.Format,
texOp.Flags & ~(TextureFlags.Offset | TextureFlags.Offsets),
+ texOp.Set,
texOp.Binding,
1 << 3, // W component: i=0, j=0
new[] { dests[destIndex++] },
@@ -527,6 +530,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
texOp.Type,
texOp.Format,
texOp.Flags & ~(TextureFlags.Offset | TextureFlags.Offsets),
+ texOp.Set,
texOp.Binding,
componentIndex,
dests,
@@ -573,6 +577,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
texOp.Type,
texOp.Format,
texOp.Flags,
+ texOp.Set,
texOp.Binding,
index,
new[] { texSizes[index] },
@@ -603,6 +608,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
texOp.Type,
texOp.Format,
texOp.Flags,
+ texOp.Set,
texOp.Binding,
0,
new[] { lod },
@@ -633,6 +639,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
texOp.Type,
texOp.Format,
texOp.Flags,
+ texOp.Set,
texOp.Binding,
index,
new[] { texSizes[index] },
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Transforms/VertexToCompute.cs b/src/Ryujinx.Graphics.Shader/Translation/Transforms/VertexToCompute.cs
index d71ada865..ddd2134d2 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Transforms/VertexToCompute.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Transforms/VertexToCompute.cs
@@ -54,6 +54,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
{
bool needsSextNorm = context.Definitions.IsAttributePackedRgb10A2Signed(location);
+ SetBindingPair setAndBinding = context.ResourceManager.Reservations.GetVertexBufferTextureSetAndBinding(location);
Operand temp = needsSextNorm ? Local() : dest;
Operand vertexElemOffset = GenerateVertexOffset(context.ResourceManager, node, location, 0);
@@ -62,7 +63,8 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
SamplerType.TextureBuffer,
TextureFormat.Unknown,
TextureFlags.IntCoords,
- context.ResourceManager.Reservations.GetVertexBufferTextureBinding(location),
+ setAndBinding.SetIndex,
+ setAndBinding.Binding,
1 << component,
new[] { temp },
new[] { vertexElemOffset }));
@@ -75,6 +77,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
}
else
{
+ SetBindingPair setAndBinding = context.ResourceManager.Reservations.GetVertexBufferTextureSetAndBinding(location);
Operand temp = component > 0 ? Local() : dest;
Operand vertexElemOffset = GenerateVertexOffset(context.ResourceManager, node, location, component);
@@ -83,7 +86,8 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms
SamplerType.TextureBuffer,
TextureFormat.Unknown,
TextureFlags.IntCoords,
- context.ResourceManager.Reservations.GetVertexBufferTextureBinding(location),
+ setAndBinding.SetIndex,
+ setAndBinding.Binding,
1,
new[] { temp },
new[] { vertexElemOffset }));
diff --git a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
index 106535588..59914736e 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
@@ -412,8 +412,8 @@ namespace Ryujinx.Graphics.Shader.Translation
if (Stage == ShaderStage.Vertex)
{
- int ibBinding = resourceManager.Reservations.IndexBufferTextureBinding;
- TextureDefinition indexBuffer = new(2, ibBinding, "ib_data", SamplerType.TextureBuffer);
+ SetBindingPair ibSetAndBinding = resourceManager.Reservations.GetIndexBufferTextureSetAndBinding();
+ TextureDefinition indexBuffer = new(ibSetAndBinding.SetIndex, ibSetAndBinding.Binding, "ib_data", SamplerType.TextureBuffer);
resourceManager.Properties.AddOrUpdateTexture(indexBuffer);
int inputMap = _program.AttributeUsage.UsedInputAttributes;
@@ -421,8 +421,8 @@ namespace Ryujinx.Graphics.Shader.Translation
while (inputMap != 0)
{
int location = BitOperations.TrailingZeroCount(inputMap);
- int binding = resourceManager.Reservations.GetVertexBufferTextureBinding(location);
- TextureDefinition vaBuffer = new(2, binding, $"vb_data{location}", SamplerType.TextureBuffer);
+ SetBindingPair setAndBinding = resourceManager.Reservations.GetVertexBufferTextureSetAndBinding(location);
+ TextureDefinition vaBuffer = new(setAndBinding.SetIndex, setAndBinding.Binding, $"vb_data{location}", SamplerType.TextureBuffer);
resourceManager.Properties.AddOrUpdateTexture(vaBuffer);
inputMap &= ~(1 << location);
@@ -430,8 +430,8 @@ namespace Ryujinx.Graphics.Shader.Translation
}
else if (Stage == ShaderStage.Geometry)
{
- int trbBinding = resourceManager.Reservations.TopologyRemapBufferTextureBinding;
- TextureDefinition remapBuffer = new(2, trbBinding, "trb_data", SamplerType.TextureBuffer);
+ SetBindingPair trbSetAndBinding = resourceManager.Reservations.GetTopologyRemapBufferTextureSetAndBinding();
+ TextureDefinition remapBuffer = new(trbSetAndBinding.SetIndex, trbSetAndBinding.Binding, "trb_data", SamplerType.TextureBuffer);
resourceManager.Properties.AddOrUpdateTexture(remapBuffer);
int geometryVbOutputSbBinding = resourceManager.Reservations.GeometryVertexOutputStorageBufferBinding;
diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs
index a0010e660..382f88d05 100644
--- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs
+++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs
@@ -69,17 +69,7 @@ namespace Ryujinx.Graphics.Vulkan
}
}
- private record struct ArrayRef
- {
- public ShaderStage Stage;
- public T Array;
-
- public ArrayRef(ShaderStage stage, T array)
- {
- Stage = stage;
- Array = array;
- }
- }
+ private readonly record struct ArrayRef(ShaderStage Stage, T Array);
private readonly VulkanRenderer _gd;
private readonly Device _device;
@@ -97,6 +87,9 @@ namespace Ryujinx.Graphics.Vulkan
private ArrayRef[] _textureArrayRefs;
private ArrayRef[] _imageArrayRefs;
+ private ArrayRef[] _textureArrayExtraRefs;
+ private ArrayRef[] _imageArrayExtraRefs;
+
private readonly DescriptorBufferInfo[] _uniformBuffers;
private readonly DescriptorBufferInfo[] _storageBuffers;
private readonly DescriptorImageInfo[] _textures;
@@ -152,6 +145,9 @@ namespace Ryujinx.Graphics.Vulkan
_textureArrayRefs = Array.Empty>();
_imageArrayRefs = Array.Empty>();
+ _textureArrayExtraRefs = Array.Empty>();
+ _imageArrayExtraRefs = Array.Empty>();
+
_uniformBuffers = new DescriptorBufferInfo[Constants.MaxUniformBufferBindings];
_storageBuffers = new DescriptorBufferInfo[Constants.MaxStorageBufferBindings];
_textures = new DescriptorImageInfo[Constants.MaxTexturesPerStage];
@@ -495,25 +491,39 @@ namespace Ryujinx.Graphics.Vulkan
public void SetTextureArray(CommandBufferScoped cbs, ShaderStage stage, int binding, ITextureArray array)
{
- if (_textureArrayRefs.Length <= binding)
- {
- Array.Resize(ref _textureArrayRefs, binding + ArrayGrowthSize);
- }
+ ref ArrayRef arrayRef = ref GetArrayRef(ref _textureArrayRefs, binding, ArrayGrowthSize);
- if (_textureArrayRefs[binding].Stage != stage || _textureArrayRefs[binding].Array != array)
+ if (arrayRef.Stage != stage || arrayRef.Array != array)
{
- if (_textureArrayRefs[binding].Array != null)
- {
- _textureArrayRefs[binding].Array.Bound = false;
- }
+ arrayRef.Array?.DecrementBindCount();
if (array is TextureArray textureArray)
{
- textureArray.Bound = true;
+ textureArray.IncrementBindCount();
textureArray.QueueWriteToReadBarriers(cbs, stage.ConvertToPipelineStageFlags());
}
- _textureArrayRefs[binding] = new ArrayRef(stage, array as TextureArray);
+ arrayRef = new ArrayRef(stage, array as TextureArray);
+
+ SignalDirty(DirtyFlags.Texture);
+ }
+ }
+
+ public void SetTextureArraySeparate(CommandBufferScoped cbs, ShaderStage stage, int setIndex, ITextureArray array)
+ {
+ ref ArrayRef arrayRef = ref GetArrayRef(ref _textureArrayExtraRefs, setIndex - PipelineBase.DescriptorSetLayouts);
+
+ if (arrayRef.Stage != stage || arrayRef.Array != array)
+ {
+ arrayRef.Array?.DecrementBindCount();
+
+ if (array is TextureArray textureArray)
+ {
+ textureArray.IncrementBindCount();
+ textureArray.QueueWriteToReadBarriers(cbs, stage.ConvertToPipelineStageFlags());
+ }
+
+ arrayRef = new ArrayRef(stage, array as TextureArray);
SignalDirty(DirtyFlags.Texture);
}
@@ -521,30 +531,56 @@ namespace Ryujinx.Graphics.Vulkan
public void SetImageArray(CommandBufferScoped cbs, ShaderStage stage, int binding, IImageArray array)
{
- if (_imageArrayRefs.Length <= binding)
- {
- Array.Resize(ref _imageArrayRefs, binding + ArrayGrowthSize);
- }
+ ref ArrayRef arrayRef = ref GetArrayRef(ref _imageArrayRefs, binding, ArrayGrowthSize);
- if (_imageArrayRefs[binding].Stage != stage || _imageArrayRefs[binding].Array != array)
+ if (arrayRef.Stage != stage || arrayRef.Array != array)
{
- if (_imageArrayRefs[binding].Array != null)
- {
- _imageArrayRefs[binding].Array.Bound = false;
- }
+ arrayRef.Array?.DecrementBindCount();
if (array is ImageArray imageArray)
{
- imageArray.Bound = true;
+ imageArray.IncrementBindCount();
imageArray.QueueWriteToReadBarriers(cbs, stage.ConvertToPipelineStageFlags());
}
- _imageArrayRefs[binding] = new ArrayRef(stage, array as ImageArray);
+ arrayRef = new ArrayRef(stage, array as ImageArray);
SignalDirty(DirtyFlags.Image);
}
}
+ public void SetImageArraySeparate(CommandBufferScoped cbs, ShaderStage stage, int setIndex, IImageArray array)
+ {
+ ref ArrayRef arrayRef = ref GetArrayRef(ref _imageArrayExtraRefs, setIndex - PipelineBase.DescriptorSetLayouts);
+
+ if (arrayRef.Stage != stage || arrayRef.Array != array)
+ {
+ arrayRef.Array?.DecrementBindCount();
+
+ if (array is ImageArray imageArray)
+ {
+ imageArray.IncrementBindCount();
+ imageArray.QueueWriteToReadBarriers(cbs, stage.ConvertToPipelineStageFlags());
+ }
+
+ arrayRef = new ArrayRef(stage, array as ImageArray);
+
+ SignalDirty(DirtyFlags.Image);
+ }
+ }
+
+ private static ref ArrayRef GetArrayRef(ref ArrayRef[] array, int index, int growthSize = 1)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(index);
+
+ if (array.Length <= index)
+ {
+ Array.Resize(ref array, index + growthSize);
+ }
+
+ return ref array[index];
+ }
+
public void SetUniformBuffers(CommandBuffer commandBuffer, ReadOnlySpan buffers)
{
for (int i = 0; i < buffers.Length; i++)
@@ -594,31 +630,40 @@ namespace Ryujinx.Graphics.Vulkan
return;
}
+ var program = _program;
+
if (_dirty.HasFlag(DirtyFlags.Uniform))
{
- if (_program.UsePushDescriptors)
+ if (program.UsePushDescriptors)
{
- UpdateAndBindUniformBufferPd(cbs, pbp);
+ UpdateAndBindUniformBufferPd(cbs);
}
else
{
- UpdateAndBind(cbs, PipelineBase.UniformSetIndex, pbp);
+ UpdateAndBind(cbs, program, PipelineBase.UniformSetIndex, pbp);
}
}
if (_dirty.HasFlag(DirtyFlags.Storage))
{
- UpdateAndBind(cbs, PipelineBase.StorageSetIndex, pbp);
+ UpdateAndBind(cbs, program, PipelineBase.StorageSetIndex, pbp);
}
if (_dirty.HasFlag(DirtyFlags.Texture))
{
- UpdateAndBind(cbs, PipelineBase.TextureSetIndex, pbp);
+ UpdateAndBind(cbs, program, PipelineBase.TextureSetIndex, pbp);
}
if (_dirty.HasFlag(DirtyFlags.Image))
{
- UpdateAndBind(cbs, PipelineBase.ImageSetIndex, pbp);
+ UpdateAndBind(cbs, program, PipelineBase.ImageSetIndex, pbp);
+ }
+
+ if (program.BindingSegments.Length > PipelineBase.DescriptorSetLayouts)
+ {
+ // Program is using extra sets, we need to bind those too.
+
+ BindExtraSets(cbs, program, pbp);
}
_dirty = DirtyFlags.None;
@@ -658,9 +703,8 @@ namespace Ryujinx.Graphics.Vulkan
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void UpdateAndBind(CommandBufferScoped cbs, int setIndex, PipelineBindPoint pbp)
+ private void UpdateAndBind(CommandBufferScoped cbs, ShaderCollection program, int setIndex, PipelineBindPoint pbp)
{
- var program = _program;
var bindingSegments = program.BindingSegments[setIndex];
if (bindingSegments.Length == 0)
@@ -869,7 +913,7 @@ namespace Ryujinx.Graphics.Vulkan
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void UpdateAndBindUniformBufferPd(CommandBufferScoped cbs, PipelineBindPoint pbp)
+ private void UpdateAndBindUniformBufferPd(CommandBufferScoped cbs)
{
int sequence = _pdSequence;
var bindingSegments = _program.BindingSegments[PipelineBase.UniformSetIndex];
@@ -933,6 +977,56 @@ namespace Ryujinx.Graphics.Vulkan
}
}
+ private void BindExtraSets(CommandBufferScoped cbs, ShaderCollection program, PipelineBindPoint pbp)
+ {
+ for (int setIndex = PipelineBase.DescriptorSetLayouts; setIndex < program.BindingSegments.Length; setIndex++)
+ {
+ var bindingSegments = program.BindingSegments[setIndex];
+
+ if (bindingSegments.Length == 0)
+ {
+ continue;
+ }
+
+ ResourceBindingSegment segment = bindingSegments[0];
+
+ if (segment.IsArray)
+ {
+ DescriptorSet[] sets = null;
+
+ if (segment.Type == ResourceType.Texture ||
+ segment.Type == ResourceType.Sampler ||
+ segment.Type == ResourceType.TextureAndSampler ||
+ segment.Type == ResourceType.BufferTexture)
+ {
+ sets = _textureArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts].Array.GetDescriptorSets(
+ _device,
+ cbs,
+ _templateUpdater,
+ program,
+ setIndex,
+ _dummyTexture,
+ _dummySampler);
+ }
+ else if (segment.Type == ResourceType.Image || segment.Type == ResourceType.BufferImage)
+ {
+ sets = _imageArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts].Array.GetDescriptorSets(
+ _device,
+ cbs,
+ _templateUpdater,
+ program,
+ setIndex,
+ _dummyTexture);
+ }
+
+ if (sets != null)
+ {
+ _gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, _program.PipelineLayout, (uint)setIndex, 1, sets, 0, ReadOnlySpan.Empty);
+ }
+ }
+ }
+ }
+
public void SignalCommandBufferChange()
{
_updateDescriptorCacheCbIndex = true;
diff --git a/src/Ryujinx.Graphics.Vulkan/ImageArray.cs b/src/Ryujinx.Graphics.Vulkan/ImageArray.cs
index 38a5b6b48..3c7f321ff 100644
--- a/src/Ryujinx.Graphics.Vulkan/ImageArray.cs
+++ b/src/Ryujinx.Graphics.Vulkan/ImageArray.cs
@@ -2,6 +2,7 @@ using Ryujinx.Graphics.GAL;
using Silk.NET.Vulkan;
using System;
using System.Collections.Generic;
+using System.Diagnostics;
namespace Ryujinx.Graphics.Vulkan
{
@@ -24,12 +25,18 @@ namespace Ryujinx.Graphics.Vulkan
private HashSet _storages;
+ private DescriptorSet[] _cachedDescriptorSets;
+
private int _cachedCommandBufferIndex;
private int _cachedSubmissionCount;
+ private ShaderCollection _cachedDscProgram;
+ private int _cachedDscSetIndex;
+ private int _cachedDscIndex;
+
private readonly bool _isBuffer;
- public bool Bound;
+ private int _bindCount;
public ImageArray(VulkanRenderer gd, int size, bool isBuffer)
{
@@ -97,8 +104,12 @@ namespace Ryujinx.Graphics.Vulkan
{
_cachedCommandBufferIndex = -1;
_storages = null;
+ _cachedDescriptorSets = null;
- _gd.PipelineInternal.ForceImageDirty();
+ if (_bindCount != 0)
+ {
+ _gd.PipelineInternal.ForceImageDirty();
+ }
}
public void QueueWriteToReadBarriers(CommandBufferScoped cbs, PipelineStageFlags stageFlags)
@@ -175,5 +186,65 @@ namespace Ryujinx.Graphics.Vulkan
return bufferTextures;
}
+
+ public DescriptorSet[] GetDescriptorSets(
+ Device device,
+ CommandBufferScoped cbs,
+ DescriptorSetTemplateUpdater templateUpdater,
+ ShaderCollection program,
+ int setIndex,
+ TextureView dummyTexture)
+ {
+ if (_cachedDescriptorSets != null)
+ {
+ // We still need to ensure the current command buffer holds a reference to all used textures.
+
+ if (!_isBuffer)
+ {
+ GetImageInfos(_gd, cbs, dummyTexture);
+ }
+ else
+ {
+ GetBufferViews(cbs);
+ }
+
+ return _cachedDescriptorSets;
+ }
+
+ _cachedDscProgram?.ReleaseManualDescriptorSetCollection(_cachedDscSetIndex, _cachedDscIndex);
+ var dsc = program.GetNewManualDescriptorSetCollection(cbs.CommandBufferIndex, setIndex, out _cachedDscIndex).Get(cbs);
+
+ DescriptorSetTemplate template = program.Templates[setIndex];
+
+ DescriptorSetTemplateWriter tu = templateUpdater.Begin(template);
+
+ if (!_isBuffer)
+ {
+ tu.Push(GetImageInfos(_gd, cbs, dummyTexture));
+ }
+ else
+ {
+ tu.Push(GetBufferViews(cbs));
+ }
+
+ var sets = dsc.GetSets();
+ templateUpdater.Commit(_gd, device, sets[0]);
+ _cachedDescriptorSets = sets;
+ _cachedDscProgram = program;
+ _cachedDscSetIndex = setIndex;
+
+ return sets;
+ }
+
+ public void IncrementBindCount()
+ {
+ _bindCount++;
+ }
+
+ public void DecrementBindCount()
+ {
+ int newBindCount = --_bindCount;
+ Debug.Assert(newBindCount >= 0);
+ }
}
}
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
index 3776e2f69..918de59b7 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
@@ -751,14 +751,12 @@ namespace Ryujinx.Graphics.Vulkan
_vertexBufferUpdater.Commit(Cbs);
}
-#pragma warning disable CA1822 // Mark member as static
public void SetAlphaTest(bool enable, float reference, CompareOp op)
{
// This is currently handled using shader specialization, as Vulkan does not support alpha test.
// In the future, we may want to use this to write the reference value into the support buffer,
// to avoid creating one version of the shader per reference value used.
}
-#pragma warning restore CA1822
public void SetBlendState(AdvancedBlendDescriptor blend)
{
@@ -903,6 +901,11 @@ namespace Ryujinx.Graphics.Vulkan
_descriptorSetUpdater.SetImageArray(Cbs, stage, binding, array);
}
+ public void SetImageArraySeparate(ShaderStage stage, int setIndex, IImageArray array)
+ {
+ _descriptorSetUpdater.SetImageArraySeparate(Cbs, stage, setIndex, array);
+ }
+
public void SetIndexBuffer(BufferRange buffer, IndexType type)
{
if (buffer.Handle != BufferHandle.Null)
@@ -945,7 +948,6 @@ namespace Ryujinx.Graphics.Vulkan
// TODO: Default levels (likely needs emulation on shaders?)
}
-#pragma warning disable CA1822 // Mark member as static
public void SetPointParameters(float size, bool isProgramPointSize, bool enablePointSprite, Origin origin)
{
// TODO.
@@ -955,7 +957,6 @@ namespace Ryujinx.Graphics.Vulkan
{
// TODO.
}
-#pragma warning restore CA1822
public void SetPrimitiveRestart(bool enable, int index)
{
@@ -1156,6 +1157,11 @@ namespace Ryujinx.Graphics.Vulkan
_descriptorSetUpdater.SetTextureArray(Cbs, stage, binding, array);
}
+ public void SetTextureArraySeparate(ShaderStage stage, int setIndex, ITextureArray array)
+ {
+ _descriptorSetUpdater.SetTextureArraySeparate(Cbs, stage, setIndex, array);
+ }
+
public void SetTransformFeedbackBuffers(ReadOnlySpan buffers)
{
PauseTransformFeedbackInternal();
@@ -1186,12 +1192,10 @@ namespace Ryujinx.Graphics.Vulkan
_descriptorSetUpdater.SetUniformBuffers(CommandBuffer, buffers);
}
-#pragma warning disable CA1822 // Mark member as static
public void SetUserClipDistance(int index, bool enableClip)
{
// TODO.
}
-#pragma warning restore CA1822
public void SetVertexAttribs(ReadOnlySpan vertexAttribs)
{
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs
index fb1f0a5ff..7d0948d6e 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs
@@ -3,6 +3,7 @@ using Silk.NET.Vulkan;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Vulkan
{
@@ -27,6 +28,24 @@ namespace Ryujinx.Graphics.Vulkan
private int _dsLastCbIndex;
private int _dsLastSubmissionCount;
+ private struct ManualDescriptorSetEntry
+ {
+ public Auto DescriptorSet;
+ public int CbIndex;
+ public int CbSubmissionCount;
+ public bool InUse;
+
+ public ManualDescriptorSetEntry(Auto descriptorSet, int cbIndex, int cbSubmissionCount, bool inUse)
+ {
+ DescriptorSet = descriptorSet;
+ CbIndex = cbIndex;
+ CbSubmissionCount = cbSubmissionCount;
+ InUse = inUse;
+ }
+ }
+
+ private readonly List[] _manualDsCache;
+
private readonly Dictionary _pdTemplates;
private readonly ResourceDescriptorCollection _pdDescriptors;
private long _lastPdUsage;
@@ -50,6 +69,7 @@ namespace Ryujinx.Graphics.Vulkan
}
_dsCacheCursor = new int[setsCount];
+ _manualDsCache = new List[setsCount];
}
public PipelineLayoutCacheEntry(
@@ -124,6 +144,51 @@ namespace Ryujinx.Graphics.Vulkan
return list[index];
}
+ public Auto GetNewManualDescriptorSetCollection(int commandBufferIndex, int setIndex, out int cacheIndex)
+ {
+ int submissionCount = _gd.CommandBufferPool.GetSubmissionCount(commandBufferIndex);
+
+ var list = _manualDsCache[setIndex] ??= new();
+ var span = CollectionsMarshal.AsSpan(list);
+
+ for (int index = 0; index < span.Length; index++)
+ {
+ ref ManualDescriptorSetEntry entry = ref span[index];
+
+ if (!entry.InUse && (entry.CbIndex != commandBufferIndex || entry.CbSubmissionCount != submissionCount))
+ {
+ entry.InUse = true;
+ entry.CbIndex = commandBufferIndex;
+ entry.CbSubmissionCount = submissionCount;
+
+ cacheIndex = index;
+
+ return entry.DescriptorSet;
+ }
+ }
+
+ var dsc = _descriptorSetManager.AllocateDescriptorSet(
+ _gd.Api,
+ DescriptorSetLayouts[setIndex],
+ _poolSizes[setIndex],
+ setIndex,
+ _consumedDescriptorsPerSet[setIndex],
+ false);
+
+ cacheIndex = list.Count;
+ list.Add(new ManualDescriptorSetEntry(dsc, commandBufferIndex, submissionCount, inUse: true));
+
+ return dsc;
+ }
+
+ public void ReleaseManualDescriptorSetCollection(int setIndex, int cacheIndex)
+ {
+ var list = _manualDsCache[setIndex];
+ var span = CollectionsMarshal.AsSpan(list);
+
+ span[cacheIndex].InUse = false;
+ }
+
private static Span GetDescriptorPoolSizes(Span output, ResourceDescriptorCollection setDescriptor, uint multiplier)
{
int count = 0;
@@ -204,6 +269,21 @@ namespace Ryujinx.Graphics.Vulkan
}
}
+ for (int i = 0; i < _manualDsCache.Length; i++)
+ {
+ if (_manualDsCache[i] == null)
+ {
+ continue;
+ }
+
+ for (int j = 0; j < _manualDsCache[i].Count; j++)
+ {
+ _manualDsCache[i][j].DescriptorSet.Dispose();
+ }
+
+ _manualDsCache[i].Clear();
+ }
+
_gd.Api.DestroyPipelineLayout(_device, PipelineLayout, null);
for (int i = 0; i < DescriptorSetLayouts.Length; i++)
diff --git a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs
index 178546983..f2d648a51 100644
--- a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs
+++ b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs
@@ -604,6 +604,16 @@ namespace Ryujinx.Graphics.Vulkan
return _plce.GetNewDescriptorSetCollection(setIndex, out isNew);
}
+ public Auto GetNewManualDescriptorSetCollection(int commandBufferIndex, int setIndex, out int cacheIndex)
+ {
+ return _plce.GetNewManualDescriptorSetCollection(commandBufferIndex, setIndex, out cacheIndex);
+ }
+
+ public void ReleaseManualDescriptorSetCollection(int setIndex, int cacheIndex)
+ {
+ _plce.ReleaseManualDescriptorSetCollection(setIndex, cacheIndex);
+ }
+
public bool HasSameLayout(ShaderCollection other)
{
return other != null && _plce == other._plce;
diff --git a/src/Ryujinx.Graphics.Vulkan/TextureArray.cs b/src/Ryujinx.Graphics.Vulkan/TextureArray.cs
index 6ef9087bc..fe834225c 100644
--- a/src/Ryujinx.Graphics.Vulkan/TextureArray.cs
+++ b/src/Ryujinx.Graphics.Vulkan/TextureArray.cs
@@ -2,6 +2,7 @@ using Ryujinx.Graphics.GAL;
using Silk.NET.Vulkan;
using System;
using System.Collections.Generic;
+using System.Diagnostics;
namespace Ryujinx.Graphics.Vulkan
{
@@ -24,12 +25,18 @@ namespace Ryujinx.Graphics.Vulkan
private HashSet _storages;
+ private DescriptorSet[] _cachedDescriptorSets;
+
private int _cachedCommandBufferIndex;
private int _cachedSubmissionCount;
+ private ShaderCollection _cachedDscProgram;
+ private int _cachedDscSetIndex;
+ private int _cachedDscIndex;
+
private readonly bool _isBuffer;
- public bool Bound;
+ private int _bindCount;
public TextureArray(VulkanRenderer gd, int size, bool isBuffer)
{
@@ -106,8 +113,12 @@ namespace Ryujinx.Graphics.Vulkan
{
_cachedCommandBufferIndex = -1;
_storages = null;
+ _cachedDescriptorSets = null;
- _gd.PipelineInternal.ForceTextureDirty();
+ if (_bindCount != 0)
+ {
+ _gd.PipelineInternal.ForceTextureDirty();
+ }
}
public void QueueWriteToReadBarriers(CommandBufferScoped cbs, PipelineStageFlags stageFlags)
@@ -190,5 +201,66 @@ namespace Ryujinx.Graphics.Vulkan
return bufferTextures;
}
+
+ public DescriptorSet[] GetDescriptorSets(
+ Device device,
+ CommandBufferScoped cbs,
+ DescriptorSetTemplateUpdater templateUpdater,
+ ShaderCollection program,
+ int setIndex,
+ TextureView dummyTexture,
+ SamplerHolder dummySampler)
+ {
+ if (_cachedDescriptorSets != null)
+ {
+ // We still need to ensure the current command buffer holds a reference to all used textures.
+
+ if (!_isBuffer)
+ {
+ GetImageInfos(_gd, cbs, dummyTexture, dummySampler);
+ }
+ else
+ {
+ GetBufferViews(cbs);
+ }
+
+ return _cachedDescriptorSets;
+ }
+
+ _cachedDscProgram?.ReleaseManualDescriptorSetCollection(_cachedDscSetIndex, _cachedDscIndex);
+ var dsc = program.GetNewManualDescriptorSetCollection(cbs.CommandBufferIndex, setIndex, out _cachedDscIndex).Get(cbs);
+
+ DescriptorSetTemplate template = program.Templates[setIndex];
+
+ DescriptorSetTemplateWriter tu = templateUpdater.Begin(template);
+
+ if (!_isBuffer)
+ {
+ tu.Push(GetImageInfos(_gd, cbs, dummyTexture, dummySampler));
+ }
+ else
+ {
+ tu.Push(GetBufferViews(cbs));
+ }
+
+ var sets = dsc.GetSets();
+ templateUpdater.Commit(_gd, device, sets[0]);
+ _cachedDescriptorSets = sets;
+ _cachedDscProgram = program;
+ _cachedDscSetIndex = setIndex;
+
+ return sets;
+ }
+
+ public void IncrementBindCount()
+ {
+ _bindCount++;
+ }
+
+ public void DecrementBindCount()
+ {
+ int newBindCount = --_bindCount;
+ Debug.Assert(newBindCount >= 0);
+ }
}
}
diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
index 175d5e3ea..86a347e01 100644
--- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
+++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
@@ -728,6 +728,12 @@ namespace Ryujinx.Graphics.Vulkan
supportsViewportSwizzle: false,
supportsIndirectParameters: true,
supportsDepthClipControl: Capabilities.SupportsDepthClipControl,
+ uniformBufferSetIndex: PipelineBase.UniformSetIndex,
+ storageBufferSetIndex: PipelineBase.StorageSetIndex,
+ textureSetIndex: PipelineBase.TextureSetIndex,
+ imageSetIndex: PipelineBase.ImageSetIndex,
+ extraSetBaseIndex: PipelineBase.DescriptorSetLayouts,
+ maximumExtraSets: Math.Max(0, (int)limits.MaxBoundDescriptorSets - PipelineBase.DescriptorSetLayouts),
maximumUniformBuffersPerStage: Constants.MaxUniformBuffersPerStage,
maximumStorageBuffersPerStage: Constants.MaxStorageBuffersPerStage,
maximumTexturesPerStage: Constants.MaxTexturesPerStage,
diff --git a/src/Ryujinx.ShaderTools/Program.cs b/src/Ryujinx.ShaderTools/Program.cs
index d2c6bd59e..a84d7b466 100644
--- a/src/Ryujinx.ShaderTools/Program.cs
+++ b/src/Ryujinx.ShaderTools/Program.cs
@@ -25,32 +25,32 @@ namespace Ryujinx.ShaderTools
_imagesCount = 0;
}
- public int CreateConstantBufferBinding(int index)
+ public SetBindingPair CreateConstantBufferBinding(int index)
{
- return index + 1;
+ return new SetBindingPair(0, index + 1);
}
- public int CreateImageBinding(int count, bool isBuffer)
+ public SetBindingPair CreateImageBinding(int count, bool isBuffer)
{
int binding = _imagesCount;
_imagesCount += count;
- return binding;
+ return new SetBindingPair(3, binding);
}
- public int CreateStorageBufferBinding(int index)
+ public SetBindingPair CreateStorageBufferBinding(int index)
{
- return index;
+ return new SetBindingPair(1, index);
}
- public int CreateTextureBinding(int count, bool isBuffer)
+ public SetBindingPair CreateTextureBinding(int count, bool isBuffer)
{
int binding = _texturesCount;
_texturesCount += count;
- return binding;
+ return new SetBindingPair(2, binding);
}
public ReadOnlySpan GetCode(ulong address, int minimumSize)
From 2ebe929fa5b9047cf925eb7d02ac4134c0f8a325 Mon Sep 17 00:00:00 2001
From: MutantAura <44103205+MutantAura@users.noreply.github.com>
Date: Sun, 26 May 2024 19:06:41 +0100
Subject: [PATCH 031/109] misc: Change disk shader cache compression algorithm
to `Brotli` (RFC 7932) (#6841)
* Prefer `Brotli` compression for disk shader cache.
* Final default case for decompression switch.
* Prefer fastest compression.
---
.../Shader/DiskCache/BinarySerializer.cs | 39 +++++++++++++++++--
.../Shader/DiskCache/CompressionAlgorithm.cs | 5 +++
.../Shader/DiskCache/DiskCacheCommon.cs | 2 +-
3 files changed, 41 insertions(+), 5 deletions(-)
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs
index c4a648fe4..ab4508f6d 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs
@@ -125,9 +125,18 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
CompressionAlgorithm algorithm = CompressionAlgorithm.None;
Read(ref algorithm);
- if (algorithm == CompressionAlgorithm.Deflate)
+ switch (algorithm)
{
- _activeStream = new DeflateStream(_stream, CompressionMode.Decompress, true);
+ case CompressionAlgorithm.None:
+ break;
+ case CompressionAlgorithm.Deflate:
+ _activeStream = new DeflateStream(_stream, CompressionMode.Decompress, true);
+ break;
+ case CompressionAlgorithm.Brotli:
+ _activeStream = new BrotliStream(_stream, CompressionMode.Decompress, true);
+ break;
+ default:
+ throw new ArgumentException($"Invalid compression algorithm \"{algorithm}\"");
}
}
@@ -139,9 +148,18 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
{
Write(ref algorithm);
- if (algorithm == CompressionAlgorithm.Deflate)
+ switch (algorithm)
{
- _activeStream = new DeflateStream(_stream, CompressionLevel.Fastest, true);
+ case CompressionAlgorithm.None:
+ break;
+ case CompressionAlgorithm.Deflate:
+ _activeStream = new DeflateStream(_stream, CompressionLevel.Fastest, true);
+ break;
+ case CompressionAlgorithm.Brotli:
+ _activeStream = new BrotliStream(_stream, CompressionLevel.Fastest, true);
+ break;
+ default:
+ throw new ArgumentException($"Invalid compression algorithm \"{algorithm}\"");
}
}
@@ -187,6 +205,14 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
}
stream.Dispose();
break;
+ case CompressionAlgorithm.Brotli:
+ stream = new BrotliStream(stream, CompressionMode.Decompress, true);
+ for (int offset = 0; offset < data.Length;)
+ {
+ offset += stream.Read(data[offset..]);
+ }
+ stream.Dispose();
+ break;
}
}
@@ -210,6 +236,11 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
stream.Write(data);
stream.Dispose();
break;
+ case CompressionAlgorithm.Brotli:
+ stream = new BrotliStream(stream, CompressionLevel.Fastest, true);
+ stream.Write(data);
+ stream.Dispose();
+ break;
}
}
}
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/CompressionAlgorithm.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/CompressionAlgorithm.cs
index 96ddbb513..86d3de07d 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/CompressionAlgorithm.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/CompressionAlgorithm.cs
@@ -14,5 +14,10 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
/// Deflate compression (RFC 1951).
///
Deflate,
+
+ ///
+ /// Brotli compression (RFC 7932).
+ ///
+ Brotli,
}
}
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheCommon.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheCommon.cs
index c4ce0b870..cecfe9acf 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheCommon.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheCommon.cs
@@ -51,7 +51,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
/// Compression algorithm
public static CompressionAlgorithm GetCompressionAlgorithm()
{
- return CompressionAlgorithm.Deflate;
+ return CompressionAlgorithm.Brotli;
}
}
}
From c41fddd25e8ddd3cf0b3cefeaf6595d2e4ede0fa Mon Sep 17 00:00:00 2001
From: gdkchan
Date: Sun, 26 May 2024 15:20:10 -0300
Subject: [PATCH 032/109] Vulkan: Extend full bindless to cover cases with phi
nodes (#6853)
* Key textures using set and binding (rather than just binding)
* Extend full bindless to cover cases with phi nodes
* Log error on bindless access failure
* Shader cache version bump
* Remove constant buffer match to reduce the chances of full bindless triggering
* Re-enable it for constant buffers, paper mario does actually need it
* Format whitespace
---
.../Shader/DiskCache/DiskCacheHostStorage.cs | 2 +-
.../Optimizations/BindlessElimination.cs | 55 +++++++++++++++----
2 files changed, 46 insertions(+), 11 deletions(-)
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
index 990c6ba3b..fbf48f017 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
@@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
private const ushort FileFormatVersionMajor = 1;
private const ushort FileFormatVersionMinor = 2;
private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
- private const uint CodeGenVersion = 6870;
+ private const uint CodeGenVersion = 6852;
private const string SharedTocFileName = "shared.toc";
private const string SharedDataFileName = "shared.data";
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
index 29501b710..02a83fbe4 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
@@ -38,6 +38,12 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
// If we can't do bindless elimination, remove the texture operation.
// Set any destination variables to zero.
+ string typeName = texOp.Inst.IsImage()
+ ? texOp.Type.ToGlslImageType(texOp.Format.GetComponentType())
+ : texOp.Type.ToGlslTextureType();
+
+ gpuAccessor.Log($"Failed to find handle source for bindless access of type \"{typeName}\".");
+
for (int destIndex = 0; destIndex < texOp.DestsCount; destIndex++)
{
block.Operations.AddBefore(node, new Operation(Instruction.Copy, texOp.GetDest(destIndex), OperandHelper.Const(0)));
@@ -62,17 +68,22 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
return false;
}
- Operand nvHandle = texOp.GetSource(0);
+ Operand bindlessHandle = texOp.GetSource(0);
- if (nvHandle.AsgOp is not Operation handleOp ||
- handleOp.Inst != Instruction.Load ||
- (handleOp.StorageKind != StorageKind.Input && handleOp.StorageKind != StorageKind.StorageBuffer))
+ if (bindlessHandle.AsgOp is PhiNode phi)
{
- // Right now, we only allow bindless access when the handle comes from a shader input or storage buffer.
- // This is an artificial limitation to prevent it from being used in cases where it
- // would have a large performance impact of loading all textures in the pool.
- // It might be removed in the future, if we can mitigate the performance impact.
+ for (int srcIndex = 0; srcIndex < phi.SourcesCount; srcIndex++)
+ {
+ Operand phiSource = phi.GetSource(srcIndex);
+ if (phiSource.AsgOp is not PhiNode && !IsBindlessAccessAllowed(phiSource))
+ {
+ return false;
+ }
+ }
+ }
+ else if (!IsBindlessAccessAllowed(bindlessHandle))
+ {
return false;
}
@@ -80,8 +91,8 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
Operand samplerHandle = OperandHelper.Local();
Operand textureIndex = OperandHelper.Local();
- block.Operations.AddBefore(node, new Operation(Instruction.BitwiseAnd, textureHandle, nvHandle, OperandHelper.Const(0xfffff)));
- block.Operations.AddBefore(node, new Operation(Instruction.ShiftRightU32, samplerHandle, nvHandle, OperandHelper.Const(20)));
+ block.Operations.AddBefore(node, new Operation(Instruction.BitwiseAnd, textureHandle, bindlessHandle, OperandHelper.Const(0xfffff)));
+ block.Operations.AddBefore(node, new Operation(Instruction.ShiftRightU32, samplerHandle, bindlessHandle, OperandHelper.Const(20)));
int texturePoolLength = Math.Max(BindlessToArray.MinimumArrayLength, gpuAccessor.QueryTextureArrayLengthFromPool());
@@ -130,6 +141,30 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
return true;
}
+ private static bool IsBindlessAccessAllowed(Operand nvHandle)
+ {
+ if (nvHandle.Type == OperandType.ConstantBuffer)
+ {
+ // Bindless access with handles from constant buffer is allowed.
+
+ return true;
+ }
+
+ if (nvHandle.AsgOp is not Operation handleOp ||
+ handleOp.Inst != Instruction.Load ||
+ (handleOp.StorageKind != StorageKind.Input && handleOp.StorageKind != StorageKind.StorageBuffer))
+ {
+ // Right now, we only allow bindless access when the handle comes from a shader input or storage buffer.
+ // This is an artificial limitation to prevent it from being used in cases where it
+ // would have a large performance impact of loading all textures in the pool.
+ // It might be removed in the future, if we can mitigate the performance impact.
+
+ return false;
+ }
+
+ return true;
+ }
+
private static bool TryConvertBindless(BasicBlock block, ResourceManager resourceManager, IGpuAccessor gpuAccessor, TextureOperation texOp)
{
if (texOp.Inst == Instruction.TextureSample || texOp.Inst.IsTextureQuery())
From 971d24aef00666df5d97cd6b0fc32e292d32240b Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 2 Jun 2024 22:10:47 +0200
Subject: [PATCH 033/109] nuget: bump Microsoft.IdentityModel.JsonWebTokens
from 7.5.2 to 7.6.0 (#6893)
Bumps [Microsoft.IdentityModel.JsonWebTokens](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet) from 7.5.2 to 7.6.0.
- [Release notes](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/releases)
- [Changelog](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/dev/CHANGELOG.md)
- [Commits](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/compare/7.5.2...7.6.0)
---
updated-dependencies:
- dependency-name: Microsoft.IdentityModel.JsonWebTokens
dependency-type: direct:production
update-type: version-update:semver-minor
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Directory.Packages.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index a93247547..c6cc01f9a 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -20,7 +20,7 @@
-
+
From 888402ecaf76c0ead448baaf52abbb3d48bb4ae9 Mon Sep 17 00:00:00 2001
From: Marco Carvalho
Date: Sun, 2 Jun 2024 17:16:48 -0300
Subject: [PATCH 034/109] Avoid inexact read with 'Stream.Read' (#6847)
---
src/ARMeilleure/CodeGen/Arm64/CodeGenContext.cs | 2 +-
src/ARMeilleure/CodeGen/X86/Assembler.cs | 2 +-
src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs | 2 +-
.../Shader/DiskCache/DiskCacheGuestStorage.cs | 4 ++--
src/Ryujinx.Gtk3/UI/Windows/AvatarWindow.cs | 2 +-
src/Ryujinx.UI.Common/App/ApplicationLibrary.cs | 2 +-
.../UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs | 2 +-
7 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/src/ARMeilleure/CodeGen/Arm64/CodeGenContext.cs b/src/ARMeilleure/CodeGen/Arm64/CodeGenContext.cs
index 12ebabddd..89b1e9e6b 100644
--- a/src/ARMeilleure/CodeGen/Arm64/CodeGenContext.cs
+++ b/src/ARMeilleure/CodeGen/Arm64/CodeGenContext.cs
@@ -237,7 +237,7 @@ namespace ARMeilleure.CodeGen.Arm64
long originalPosition = _stream.Position;
_stream.Seek(0, SeekOrigin.Begin);
- _stream.Read(code, 0, code.Length);
+ _stream.ReadExactly(code, 0, code.Length);
_stream.Seek(originalPosition, SeekOrigin.Begin);
RelocInfo relocInfo;
diff --git a/src/ARMeilleure/CodeGen/X86/Assembler.cs b/src/ARMeilleure/CodeGen/X86/Assembler.cs
index 55bf07248..96f4de049 100644
--- a/src/ARMeilleure/CodeGen/X86/Assembler.cs
+++ b/src/ARMeilleure/CodeGen/X86/Assembler.cs
@@ -1444,7 +1444,7 @@ namespace ARMeilleure.CodeGen.X86
Span buffer = new byte[jump.JumpPosition - _stream.Position];
- _stream.Read(buffer);
+ _stream.ReadExactly(buffer);
_stream.Seek(ReservedBytesForJump, SeekOrigin.Current);
codeStream.Write(buffer);
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs
index ab4508f6d..3837092c9 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/BinarySerializer.cs
@@ -195,7 +195,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
switch (algorithm)
{
case CompressionAlgorithm.None:
- stream.Read(data);
+ stream.ReadExactly(data);
break;
case CompressionAlgorithm.Deflate:
stream = new DeflateStream(stream, CompressionMode.Decompress, true);
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGuestStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGuestStorage.cs
index 59d2cfb3f..08cd3bb02 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGuestStorage.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheGuestStorage.cs
@@ -220,7 +220,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
}
dataFileStream.Seek((long)entry.Offset, SeekOrigin.Begin);
- dataFileStream.Read(cb1Data);
+ dataFileStream.ReadExactly(cb1Data);
BinarySerializer.ReadCompressed(dataFileStream, guestCode);
_cache[index] = (guestCode, cb1Data);
@@ -279,7 +279,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
dataFileStream.Seek((long)entry.Offset, SeekOrigin.Begin);
byte[] cachedCode = new byte[entry.CodeSize];
byte[] cachedCb1Data = new byte[entry.Cb1DataSize];
- dataFileStream.Read(cachedCb1Data);
+ dataFileStream.ReadExactly(cachedCb1Data);
BinarySerializer.ReadCompressed(dataFileStream, cachedCode);
if (data.SequenceEqual(cachedCode) && cb1Data.SequenceEqual(cachedCb1Data))
diff --git a/src/Ryujinx.Gtk3/UI/Windows/AvatarWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/AvatarWindow.cs
index 7cddc362b..d9ecd47b7 100644
--- a/src/Ryujinx.Gtk3/UI/Windows/AvatarWindow.cs
+++ b/src/Ryujinx.Gtk3/UI/Windows/AvatarWindow.cs
@@ -233,7 +233,7 @@ namespace Ryujinx.UI.Windows
reader.ReadInt64(); // Padding
byte[] input = new byte[stream.Length - stream.Position];
- stream.Read(input, 0, input.Length);
+ stream.ReadExactly(input, 0, input.Length);
long inputOffset = 0;
diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
index 82783e638..176011dde 100644
--- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
+++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
@@ -65,7 +65,7 @@ namespace Ryujinx.UI.App.Common
Stream resourceStream = Assembly.GetCallingAssembly().GetManifestResourceStream(resourceName);
byte[] resourceByteArray = new byte[resourceStream.Length];
- resourceStream.Read(resourceByteArray);
+ resourceStream.ReadExactly(resourceByteArray);
return resourceByteArray;
}
diff --git a/src/Ryujinx/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs b/src/Ryujinx/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs
index 89b591229..12adfe94b 100644
--- a/src/Ryujinx/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs
@@ -151,7 +151,7 @@ namespace Ryujinx.Ava.UI.ViewModels
reader.ReadInt64(); // Padding
byte[] input = new byte[stream.Length - stream.Position];
- stream.Read(input, 0, input.Length);
+ stream.ReadExactly(input, 0, input.Length);
uint inputOffset = 0;
From 1ecc8fbc3b395f8238d4e74f06a8c014336d25b7 Mon Sep 17 00:00:00 2001
From: jhorv <38920027+jhorv@users.noreply.github.com>
Date: Sun, 2 Jun 2024 21:24:14 -0400
Subject: [PATCH 035/109] New pooled memory types (#6821)
* feat: add new types MemoryOwner and SpanOwner
* use SpanOwner instead of new array allocation
* change for loop condition to `fences.Length` instead of `count` to elide Span boundary checks on `fences`
---
src/Ryujinx.Common/Memory/MemoryOwner.cs | 140 ++++++++++++++++++
src/Ryujinx.Common/Memory/SpanOwner.cs | 114 ++++++++++++++
.../MultiFenceHolder.cs | 6 +-
3 files changed, 258 insertions(+), 2 deletions(-)
create mode 100644 src/Ryujinx.Common/Memory/MemoryOwner.cs
create mode 100644 src/Ryujinx.Common/Memory/SpanOwner.cs
diff --git a/src/Ryujinx.Common/Memory/MemoryOwner.cs b/src/Ryujinx.Common/Memory/MemoryOwner.cs
new file mode 100644
index 000000000..5e567ab8d
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/MemoryOwner.cs
@@ -0,0 +1,140 @@
+#nullable enable
+using System;
+using System.Buffers;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+namespace Ryujinx.Common.Memory
+{
+ ///
+ /// An implementation with an embedded length and fast
+ /// accessor, with memory allocated from .
+ ///
+ /// The type of item to store.
+ public sealed class MemoryOwner : IMemoryOwner
+ {
+ private readonly int _length;
+ private T[]? _array;
+
+ ///
+ /// Initializes a new instance of the class with the specified parameters.
+ ///
+ /// The length of the new memory buffer to use
+ private MemoryOwner(int length)
+ {
+ _length = length;
+ _array = ArrayPool.Shared.Rent(length);
+ }
+
+ ///
+ /// Creates a new instance with the specified length.
+ ///
+ /// The length of the new memory buffer to use
+ /// A instance of the requested length
+ /// Thrown when is not valid
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static MemoryOwner Rent(int length) => new(length);
+
+ ///
+ /// Creates a new instance with the specified length and the content cleared.
+ ///
+ /// The length of the new memory buffer to use
+ /// A instance of the requested length and the content cleared
+ /// Thrown when is not valid
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static MemoryOwner RentCleared(int length)
+ {
+ MemoryOwner result = new(length);
+
+ result._array.AsSpan(0, length).Clear();
+
+ return result;
+ }
+
+ ///
+ /// Creates a new instance with the content copied from the specified buffer.
+ ///
+ /// The buffer to copy
+ /// A instance with the same length and content as
+ public static MemoryOwner RentCopy(ReadOnlySpan buffer)
+ {
+ MemoryOwner result = new(buffer.Length);
+
+ buffer.CopyTo(result._array);
+
+ return result;
+ }
+
+ ///
+ /// Gets the number of items in the current instance.
+ ///
+ public int Length
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _length;
+ }
+
+ ///
+ public Memory Memory
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ T[]? array = _array;
+
+ if (array is null)
+ {
+ ThrowObjectDisposedException();
+ }
+
+ return new(array, 0, _length);
+ }
+ }
+
+ ///
+ /// Gets a wrapping the memory belonging to the current instance.
+ ///
+ ///
+ /// Uses a trick made possible by the .NET 6+ runtime array layout.
+ ///
+ public Span Span
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ T[]? array = _array;
+
+ if (array is null)
+ {
+ ThrowObjectDisposedException();
+ }
+
+ ref T firstElementRef = ref MemoryMarshal.GetArrayDataReference(array);
+
+ return MemoryMarshal.CreateSpan(ref firstElementRef, _length);
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ T[]? array = Interlocked.Exchange(ref _array, null);
+
+ if (array is not null)
+ {
+ ArrayPool.Shared.Return(array);
+ }
+ }
+
+ ///
+ /// Throws an when is .
+ ///
+ [DoesNotReturn]
+ private static void ThrowObjectDisposedException()
+ {
+ throw new ObjectDisposedException(nameof(MemoryOwner), "The buffer has already been disposed.");
+ }
+ }
+}
diff --git a/src/Ryujinx.Common/Memory/SpanOwner.cs b/src/Ryujinx.Common/Memory/SpanOwner.cs
new file mode 100644
index 000000000..a4b4adf32
--- /dev/null
+++ b/src/Ryujinx.Common/Memory/SpanOwner.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Buffers;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Common.Memory
+{
+ ///
+ /// A stack-only type that rents a buffer of a specified length from .
+ /// It does not implement to avoid being boxed, but should still be disposed. This
+ /// is easy since C# 8, which allows use of C# `using` constructs on any type that has a public Dispose() method.
+ /// To keep this type simple, fast, and read-only, it does not check or guard against multiple disposals.
+ /// For all these reasons, all usage should be with a `using` block or statement.
+ ///
+ /// The type of item to store.
+ public readonly ref struct SpanOwner
+ {
+ private readonly int _length;
+ private readonly T[] _array;
+
+ ///
+ /// Initializes a new instance of the struct with the specified parameters.
+ ///
+ /// The length of the new memory buffer to use
+ private SpanOwner(int length)
+ {
+ _length = length;
+ _array = ArrayPool.Shared.Rent(length);
+ }
+
+ ///
+ /// Gets an empty instance.
+ ///
+ public static SpanOwner Empty
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => new(0);
+ }
+
+ ///
+ /// Creates a new instance with the specified length.
+ ///
+ /// The length of the new memory buffer to use
+ /// A instance of the requested length
+ /// Thrown when is not valid
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static SpanOwner Rent(int length) => new(length);
+
+ ///
+ /// Creates a new instance with the length and the content cleared.
+ ///
+ /// The length of the new memory buffer to use
+ /// A instance of the requested length and the content cleared
+ /// Thrown when is not valid
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static SpanOwner RentCleared(int length)
+ {
+ SpanOwner result = new(length);
+
+ result._array.AsSpan(0, length).Clear();
+
+ return result;
+ }
+
+ ///
+ /// Creates a new instance with the content copied from the specified buffer.
+ ///
+ /// The buffer to copy
+ /// A instance with the same length and content as
+ public static SpanOwner RentCopy(ReadOnlySpan buffer)
+ {
+ SpanOwner result = new(buffer.Length);
+
+ buffer.CopyTo(result._array);
+
+ return result;
+ }
+
+ ///
+ /// Gets the number of items in the current instance
+ ///
+ public int Length
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _length;
+ }
+
+ ///
+ /// Gets a wrapping the memory belonging to the current instance.
+ ///
+ ///
+ /// Uses a trick made possible by the .NET 6+ runtime array layout.
+ ///
+ public Span Span
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get
+ {
+ ref T firstElementRef = ref MemoryMarshal.GetArrayDataReference(_array);
+
+ return MemoryMarshal.CreateSpan(ref firstElementRef, _length);
+ }
+ }
+
+ ///
+ /// Implements the duck-typed method.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Dispose()
+ {
+ ArrayPool.Shared.Return(_array);
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs b/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs
index 0bce3b72d..806b872bc 100644
--- a/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs
+++ b/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs
@@ -1,3 +1,4 @@
+using Ryujinx.Common.Memory;
using Silk.NET.Vulkan;
using System;
@@ -165,14 +166,15 @@ namespace Ryujinx.Graphics.Vulkan
/// True if all fences were signaled before the timeout expired, false otherwise
private bool WaitForFencesImpl(Vk api, Device device, int offset, int size, bool hasTimeout, ulong timeout)
{
- Span fenceHolders = new FenceHolder[CommandBufferPool.MaxCommandBuffers];
+ using SpanOwner fenceHoldersOwner = SpanOwner.Rent(CommandBufferPool.MaxCommandBuffers);
+ Span fenceHolders = fenceHoldersOwner.Span;
int count = size != 0 ? GetOverlappingFences(fenceHolders, offset, size) : GetFences(fenceHolders);
Span fences = stackalloc Fence[count];
int fenceCount = 0;
- for (int i = 0; i < count; i++)
+ for (int i = 0; i < fences.Length; i++)
{
if (fenceHolders[i].TryGet(out Fence fence))
{
From d7c6474729ee36875cf387629afe1389655311f8 Mon Sep 17 00:00:00 2001
From: sunshineinabox
Date: Sun, 2 Jun 2024 18:32:10 -0700
Subject: [PATCH 036/109] GPU: Remove unused dynamic state and pipeline
settings (#6796)
* Dynamic state for Depth Bounds should not be passed to PipelineDynamicStateCreateInfo as the command to set them is never called.
Do not pass pointer to viewport and scissor as those dynamic states should be supported on all devices.
Same as above for DepthBias values.
* Code Review Suggestion
* Pipeline derivation is not implemented and is not suggested.
* Depth Bounds are not used.
---
.../PipelineConverter.cs | 9 -
src/Ryujinx.Graphics.Vulkan/PipelineState.cs | 199 ++++++++----------
src/Ryujinx.Graphics.Vulkan/PipelineUid.cs | 18 +-
3 files changed, 92 insertions(+), 134 deletions(-)
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs b/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs
index 41618c736..7d124c830 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineConverter.cs
@@ -180,9 +180,6 @@ namespace Ryujinx.Graphics.Vulkan
pipeline.LogicOpEnable = state.LogicOpEnable;
pipeline.LogicOp = state.LogicOp.Convert();
- pipeline.MinDepthBounds = 0f; // Not implemented.
- pipeline.MaxDepthBounds = 0f; // Not implemented.
-
pipeline.PatchControlPoints = state.PatchControlPoints;
pipeline.PolygonMode = PolygonMode.Fill; // Not implemented.
pipeline.PrimitiveRestartEnable = state.PrimitiveRestartEnable;
@@ -208,17 +205,11 @@ namespace Ryujinx.Graphics.Vulkan
pipeline.StencilFrontPassOp = state.StencilTest.FrontDpPass.Convert();
pipeline.StencilFrontDepthFailOp = state.StencilTest.FrontDpFail.Convert();
pipeline.StencilFrontCompareOp = state.StencilTest.FrontFunc.Convert();
- pipeline.StencilFrontCompareMask = 0;
- pipeline.StencilFrontWriteMask = 0;
- pipeline.StencilFrontReference = 0;
pipeline.StencilBackFailOp = state.StencilTest.BackSFail.Convert();
pipeline.StencilBackPassOp = state.StencilTest.BackDpPass.Convert();
pipeline.StencilBackDepthFailOp = state.StencilTest.BackDpFail.Convert();
pipeline.StencilBackCompareOp = state.StencilTest.BackFunc.Convert();
- pipeline.StencilBackCompareMask = 0;
- pipeline.StencilBackWriteMask = 0;
- pipeline.StencilBackReference = 0;
pipeline.StencilTestEnable = state.StencilTest.TestEnable;
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineState.cs b/src/Ryujinx.Graphics.Vulkan/PipelineState.cs
index c38748936..2a8f93081 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineState.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineState.cs
@@ -71,244 +71,232 @@ namespace Ryujinx.Graphics.Vulkan
set => Internal.Id4 = (Internal.Id4 & 0xFFFFFFFF) | ((ulong)value << 32);
}
- public float MinDepthBounds
- {
- readonly get => BitConverter.Int32BitsToSingle((int)((Internal.Id5 >> 0) & 0xFFFFFFFF));
- set => Internal.Id5 = (Internal.Id5 & 0xFFFFFFFF00000000) | ((ulong)(uint)BitConverter.SingleToInt32Bits(value) << 0);
- }
-
- public float MaxDepthBounds
- {
- readonly get => BitConverter.Int32BitsToSingle((int)((Internal.Id5 >> 32) & 0xFFFFFFFF));
- set => Internal.Id5 = (Internal.Id5 & 0xFFFFFFFF) | ((ulong)(uint)BitConverter.SingleToInt32Bits(value) << 32);
- }
-
public PolygonMode PolygonMode
{
- readonly get => (PolygonMode)((Internal.Id6 >> 0) & 0x3FFFFFFF);
- set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFFFC0000000) | ((ulong)value << 0);
+ readonly get => (PolygonMode)((Internal.Id5 >> 0) & 0x3FFFFFFF);
+ set => Internal.Id5 = (Internal.Id5 & 0xFFFFFFFFC0000000) | ((ulong)value << 0);
}
public uint StagesCount
{
- readonly get => (byte)((Internal.Id6 >> 30) & 0xFF);
- set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFC03FFFFFFF) | ((ulong)value << 30);
+ readonly get => (byte)((Internal.Id5 >> 30) & 0xFF);
+ set => Internal.Id5 = (Internal.Id5 & 0xFFFFFFC03FFFFFFF) | ((ulong)value << 30);
}
public uint VertexAttributeDescriptionsCount
{
- readonly get => (byte)((Internal.Id6 >> 38) & 0xFF);
- set => Internal.Id6 = (Internal.Id6 & 0xFFFFC03FFFFFFFFF) | ((ulong)value << 38);
+ readonly get => (byte)((Internal.Id5 >> 38) & 0xFF);
+ set => Internal.Id5 = (Internal.Id5 & 0xFFFFC03FFFFFFFFF) | ((ulong)value << 38);
}
public uint VertexBindingDescriptionsCount
{
- readonly get => (byte)((Internal.Id6 >> 46) & 0xFF);
- set => Internal.Id6 = (Internal.Id6 & 0xFFC03FFFFFFFFFFF) | ((ulong)value << 46);
+ readonly get => (byte)((Internal.Id5 >> 46) & 0xFF);
+ set => Internal.Id5 = (Internal.Id5 & 0xFFC03FFFFFFFFFFF) | ((ulong)value << 46);
}
public uint ViewportsCount
{
- readonly get => (byte)((Internal.Id6 >> 54) & 0xFF);
- set => Internal.Id6 = (Internal.Id6 & 0xC03FFFFFFFFFFFFF) | ((ulong)value << 54);
+ readonly get => (byte)((Internal.Id5 >> 54) & 0xFF);
+ set => Internal.Id5 = (Internal.Id5 & 0xC03FFFFFFFFFFFFF) | ((ulong)value << 54);
}
public uint ScissorsCount
{
- readonly get => (byte)((Internal.Id7 >> 0) & 0xFF);
- set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFFFFFFFFF00) | ((ulong)value << 0);
+ readonly get => (byte)((Internal.Id6 >> 0) & 0xFF);
+ set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFFFFFFFFF00) | ((ulong)value << 0);
}
public uint ColorBlendAttachmentStateCount
{
- readonly get => (byte)((Internal.Id7 >> 8) & 0xFF);
- set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFFFFFFF00FF) | ((ulong)value << 8);
+ readonly get => (byte)((Internal.Id6 >> 8) & 0xFF);
+ set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFFFFFFF00FF) | ((ulong)value << 8);
}
public PrimitiveTopology Topology
{
- readonly get => (PrimitiveTopology)((Internal.Id7 >> 16) & 0xF);
- set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFFFFFF0FFFF) | ((ulong)value << 16);
+ readonly get => (PrimitiveTopology)((Internal.Id6 >> 16) & 0xF);
+ set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFFFFFF0FFFF) | ((ulong)value << 16);
}
public LogicOp LogicOp
{
- readonly get => (LogicOp)((Internal.Id7 >> 20) & 0xF);
- set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFFFFF0FFFFF) | ((ulong)value << 20);
+ readonly get => (LogicOp)((Internal.Id6 >> 20) & 0xF);
+ set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFFFFF0FFFFF) | ((ulong)value << 20);
}
public CompareOp DepthCompareOp
{
- readonly get => (CompareOp)((Internal.Id7 >> 24) & 0x7);
- set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFFFF8FFFFFF) | ((ulong)value << 24);
+ readonly get => (CompareOp)((Internal.Id6 >> 24) & 0x7);
+ set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFFFF8FFFFFF) | ((ulong)value << 24);
}
public StencilOp StencilFrontFailOp
{
- readonly get => (StencilOp)((Internal.Id7 >> 27) & 0x7);
- set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFFFC7FFFFFF) | ((ulong)value << 27);
+ readonly get => (StencilOp)((Internal.Id6 >> 27) & 0x7);
+ set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFFFC7FFFFFF) | ((ulong)value << 27);
}
public StencilOp StencilFrontPassOp
{
- readonly get => (StencilOp)((Internal.Id7 >> 30) & 0x7);
- set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFFE3FFFFFFF) | ((ulong)value << 30);
+ readonly get => (StencilOp)((Internal.Id6 >> 30) & 0x7);
+ set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFFE3FFFFFFF) | ((ulong)value << 30);
}
public StencilOp StencilFrontDepthFailOp
{
- readonly get => (StencilOp)((Internal.Id7 >> 33) & 0x7);
- set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFF1FFFFFFFF) | ((ulong)value << 33);
+ readonly get => (StencilOp)((Internal.Id6 >> 33) & 0x7);
+ set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFF1FFFFFFFF) | ((ulong)value << 33);
}
public CompareOp StencilFrontCompareOp
{
- readonly get => (CompareOp)((Internal.Id7 >> 36) & 0x7);
- set => Internal.Id7 = (Internal.Id7 & 0xFFFFFF8FFFFFFFFF) | ((ulong)value << 36);
+ readonly get => (CompareOp)((Internal.Id6 >> 36) & 0x7);
+ set => Internal.Id6 = (Internal.Id6 & 0xFFFFFF8FFFFFFFFF) | ((ulong)value << 36);
}
public StencilOp StencilBackFailOp
{
- readonly get => (StencilOp)((Internal.Id7 >> 39) & 0x7);
- set => Internal.Id7 = (Internal.Id7 & 0xFFFFFC7FFFFFFFFF) | ((ulong)value << 39);
+ readonly get => (StencilOp)((Internal.Id6 >> 39) & 0x7);
+ set => Internal.Id6 = (Internal.Id6 & 0xFFFFFC7FFFFFFFFF) | ((ulong)value << 39);
}
public StencilOp StencilBackPassOp
{
- readonly get => (StencilOp)((Internal.Id7 >> 42) & 0x7);
- set => Internal.Id7 = (Internal.Id7 & 0xFFFFE3FFFFFFFFFF) | ((ulong)value << 42);
+ readonly get => (StencilOp)((Internal.Id6 >> 42) & 0x7);
+ set => Internal.Id6 = (Internal.Id6 & 0xFFFFE3FFFFFFFFFF) | ((ulong)value << 42);
}
public StencilOp StencilBackDepthFailOp
{
- readonly get => (StencilOp)((Internal.Id7 >> 45) & 0x7);
- set => Internal.Id7 = (Internal.Id7 & 0xFFFF1FFFFFFFFFFF) | ((ulong)value << 45);
+ readonly get => (StencilOp)((Internal.Id6 >> 45) & 0x7);
+ set => Internal.Id6 = (Internal.Id6 & 0xFFFF1FFFFFFFFFFF) | ((ulong)value << 45);
}
public CompareOp StencilBackCompareOp
{
- readonly get => (CompareOp)((Internal.Id7 >> 48) & 0x7);
- set => Internal.Id7 = (Internal.Id7 & 0xFFF8FFFFFFFFFFFF) | ((ulong)value << 48);
+ readonly get => (CompareOp)((Internal.Id6 >> 48) & 0x7);
+ set => Internal.Id6 = (Internal.Id6 & 0xFFF8FFFFFFFFFFFF) | ((ulong)value << 48);
}
public CullModeFlags CullMode
{
- readonly get => (CullModeFlags)((Internal.Id7 >> 51) & 0x3);
- set => Internal.Id7 = (Internal.Id7 & 0xFFE7FFFFFFFFFFFF) | ((ulong)value << 51);
+ readonly get => (CullModeFlags)((Internal.Id6 >> 51) & 0x3);
+ set => Internal.Id6 = (Internal.Id6 & 0xFFE7FFFFFFFFFFFF) | ((ulong)value << 51);
}
public bool PrimitiveRestartEnable
{
- readonly get => ((Internal.Id7 >> 53) & 0x1) != 0UL;
- set => Internal.Id7 = (Internal.Id7 & 0xFFDFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 53);
+ readonly get => ((Internal.Id6 >> 53) & 0x1) != 0UL;
+ set => Internal.Id6 = (Internal.Id6 & 0xFFDFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 53);
}
public bool DepthClampEnable
{
- readonly get => ((Internal.Id7 >> 54) & 0x1) != 0UL;
- set => Internal.Id7 = (Internal.Id7 & 0xFFBFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 54);
+ readonly get => ((Internal.Id6 >> 54) & 0x1) != 0UL;
+ set => Internal.Id6 = (Internal.Id6 & 0xFFBFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 54);
}
public bool RasterizerDiscardEnable
{
- readonly get => ((Internal.Id7 >> 55) & 0x1) != 0UL;
- set => Internal.Id7 = (Internal.Id7 & 0xFF7FFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 55);
+ readonly get => ((Internal.Id6 >> 55) & 0x1) != 0UL;
+ set => Internal.Id6 = (Internal.Id6 & 0xFF7FFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 55);
}
public FrontFace FrontFace
{
- readonly get => (FrontFace)((Internal.Id7 >> 56) & 0x1);
- set => Internal.Id7 = (Internal.Id7 & 0xFEFFFFFFFFFFFFFF) | ((ulong)value << 56);
+ readonly get => (FrontFace)((Internal.Id6 >> 56) & 0x1);
+ set => Internal.Id6 = (Internal.Id6 & 0xFEFFFFFFFFFFFFFF) | ((ulong)value << 56);
}
public bool DepthBiasEnable
{
- readonly get => ((Internal.Id7 >> 57) & 0x1) != 0UL;
- set => Internal.Id7 = (Internal.Id7 & 0xFDFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 57);
+ readonly get => ((Internal.Id6 >> 57) & 0x1) != 0UL;
+ set => Internal.Id6 = (Internal.Id6 & 0xFDFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 57);
}
public bool DepthTestEnable
{
- readonly get => ((Internal.Id7 >> 58) & 0x1) != 0UL;
- set => Internal.Id7 = (Internal.Id7 & 0xFBFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 58);
+ readonly get => ((Internal.Id6 >> 58) & 0x1) != 0UL;
+ set => Internal.Id6 = (Internal.Id6 & 0xFBFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 58);
}
public bool DepthWriteEnable
{
- readonly get => ((Internal.Id7 >> 59) & 0x1) != 0UL;
- set => Internal.Id7 = (Internal.Id7 & 0xF7FFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 59);
+ readonly get => ((Internal.Id6 >> 59) & 0x1) != 0UL;
+ set => Internal.Id6 = (Internal.Id6 & 0xF7FFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 59);
}
public bool DepthBoundsTestEnable
{
- readonly get => ((Internal.Id7 >> 60) & 0x1) != 0UL;
- set => Internal.Id7 = (Internal.Id7 & 0xEFFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 60);
+ readonly get => ((Internal.Id6 >> 60) & 0x1) != 0UL;
+ set => Internal.Id6 = (Internal.Id6 & 0xEFFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 60);
}
public bool StencilTestEnable
{
- readonly get => ((Internal.Id7 >> 61) & 0x1) != 0UL;
- set => Internal.Id7 = (Internal.Id7 & 0xDFFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 61);
+ readonly get => ((Internal.Id6 >> 61) & 0x1) != 0UL;
+ set => Internal.Id6 = (Internal.Id6 & 0xDFFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 61);
}
public bool LogicOpEnable
{
- readonly get => ((Internal.Id7 >> 62) & 0x1) != 0UL;
- set => Internal.Id7 = (Internal.Id7 & 0xBFFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 62);
+ readonly get => ((Internal.Id6 >> 62) & 0x1) != 0UL;
+ set => Internal.Id6 = (Internal.Id6 & 0xBFFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 62);
}
public bool HasDepthStencil
{
- readonly get => ((Internal.Id7 >> 63) & 0x1) != 0UL;
- set => Internal.Id7 = (Internal.Id7 & 0x7FFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 63);
+ readonly get => ((Internal.Id6 >> 63) & 0x1) != 0UL;
+ set => Internal.Id6 = (Internal.Id6 & 0x7FFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 63);
}
public uint PatchControlPoints
{
- readonly get => (uint)((Internal.Id8 >> 0) & 0xFFFFFFFF);
- set => Internal.Id8 = (Internal.Id8 & 0xFFFFFFFF00000000) | ((ulong)value << 0);
+ readonly get => (uint)((Internal.Id7 >> 0) & 0xFFFFFFFF);
+ set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFFF00000000) | ((ulong)value << 0);
}
public uint SamplesCount
{
- readonly get => (uint)((Internal.Id8 >> 32) & 0xFFFFFFFF);
- set => Internal.Id8 = (Internal.Id8 & 0xFFFFFFFF) | ((ulong)value << 32);
+ readonly get => (uint)((Internal.Id7 >> 32) & 0xFFFFFFFF);
+ set => Internal.Id7 = (Internal.Id7 & 0xFFFFFFFF) | ((ulong)value << 32);
}
public bool AlphaToCoverageEnable
{
- readonly get => ((Internal.Id9 >> 0) & 0x1) != 0UL;
- set => Internal.Id9 = (Internal.Id9 & 0xFFFFFFFFFFFFFFFE) | ((value ? 1UL : 0UL) << 0);
+ readonly get => ((Internal.Id8 >> 0) & 0x1) != 0UL;
+ set => Internal.Id8 = (Internal.Id8 & 0xFFFFFFFFFFFFFFFE) | ((value ? 1UL : 0UL) << 0);
}
public bool AlphaToOneEnable
{
- readonly get => ((Internal.Id9 >> 1) & 0x1) != 0UL;
- set => Internal.Id9 = (Internal.Id9 & 0xFFFFFFFFFFFFFFFD) | ((value ? 1UL : 0UL) << 1);
+ readonly get => ((Internal.Id8 >> 1) & 0x1) != 0UL;
+ set => Internal.Id8 = (Internal.Id8 & 0xFFFFFFFFFFFFFFFD) | ((value ? 1UL : 0UL) << 1);
}
public bool AdvancedBlendSrcPreMultiplied
{
- readonly get => ((Internal.Id9 >> 2) & 0x1) != 0UL;
- set => Internal.Id9 = (Internal.Id9 & 0xFFFFFFFFFFFFFFFB) | ((value ? 1UL : 0UL) << 2);
+ readonly get => ((Internal.Id8 >> 2) & 0x1) != 0UL;
+ set => Internal.Id8 = (Internal.Id8 & 0xFFFFFFFFFFFFFFFB) | ((value ? 1UL : 0UL) << 2);
}
public bool AdvancedBlendDstPreMultiplied
{
- readonly get => ((Internal.Id9 >> 3) & 0x1) != 0UL;
- set => Internal.Id9 = (Internal.Id9 & 0xFFFFFFFFFFFFFFF7) | ((value ? 1UL : 0UL) << 3);
+ readonly get => ((Internal.Id8 >> 3) & 0x1) != 0UL;
+ set => Internal.Id8 = (Internal.Id8 & 0xFFFFFFFFFFFFFFF7) | ((value ? 1UL : 0UL) << 3);
}
public BlendOverlapEXT AdvancedBlendOverlap
{
- readonly get => (BlendOverlapEXT)((Internal.Id9 >> 4) & 0x3);
- set => Internal.Id9 = (Internal.Id9 & 0xFFFFFFFFFFFFFFCF) | ((ulong)value << 4);
+ readonly get => (BlendOverlapEXT)((Internal.Id8 >> 4) & 0x3);
+ set => Internal.Id8 = (Internal.Id8 & 0xFFFFFFFFFFFFFFCF) | ((ulong)value << 4);
}
public bool DepthMode
{
- readonly get => ((Internal.Id9 >> 6) & 0x1) != 0UL;
- set => Internal.Id9 = (Internal.Id9 & 0xFFFFFFFFFFFFFFBF) | ((value ? 1UL : 0UL) << 6);
+ readonly get => ((Internal.Id8 >> 6) & 0x1) != 0UL;
+ set => Internal.Id8 = (Internal.Id8 & 0xFFFFFFFFFFFFFFBF) | ((value ? 1UL : 0UL) << 6);
}
public bool HasTessellationControlShader;
@@ -408,8 +396,6 @@ namespace Ryujinx.Graphics.Vulkan
fixed (VertexInputAttributeDescription* pVertexAttributeDescriptions = &Internal.VertexAttributeDescriptions[0])
fixed (VertexInputAttributeDescription* pVertexAttributeDescriptions2 = &_vertexAttributeDescriptions2[0])
fixed (VertexInputBindingDescription* pVertexBindingDescriptions = &Internal.VertexBindingDescriptions[0])
- fixed (Viewport* pViewports = &Internal.Viewports[0])
- fixed (Rect2D* pScissors = &Internal.Scissors[0])
fixed (PipelineColorBlendAttachmentState* pColorBlendAttachmentState = &Internal.ColorBlendAttachmentState[0])
{
var vertexInputState = new PipelineVertexInputStateCreateInfo
@@ -472,18 +458,13 @@ namespace Ryujinx.Graphics.Vulkan
CullMode = CullMode,
FrontFace = FrontFace,
DepthBiasEnable = DepthBiasEnable,
- DepthBiasClamp = DepthBiasClamp,
- DepthBiasConstantFactor = DepthBiasConstantFactor,
- DepthBiasSlopeFactor = DepthBiasSlopeFactor,
};
var viewportState = new PipelineViewportStateCreateInfo
{
SType = StructureType.PipelineViewportStateCreateInfo,
ViewportCount = ViewportsCount,
- PViewports = pViewports,
ScissorCount = ScissorsCount,
- PScissors = pScissors,
};
if (gd.Capabilities.SupportsDepthClipControl)
@@ -511,19 +492,13 @@ namespace Ryujinx.Graphics.Vulkan
StencilFrontFailOp,
StencilFrontPassOp,
StencilFrontDepthFailOp,
- StencilFrontCompareOp,
- StencilFrontCompareMask,
- StencilFrontWriteMask,
- StencilFrontReference);
+ StencilFrontCompareOp);
var stencilBack = new StencilOpState(
StencilBackFailOp,
StencilBackPassOp,
StencilBackDepthFailOp,
- StencilBackCompareOp,
- StencilBackCompareMask,
- StencilBackWriteMask,
- StencilBackReference);
+ StencilBackCompareOp);
var depthStencilState = new PipelineDepthStencilStateCreateInfo
{
@@ -531,12 +506,10 @@ namespace Ryujinx.Graphics.Vulkan
DepthTestEnable = DepthTestEnable,
DepthWriteEnable = DepthWriteEnable,
DepthCompareOp = DepthCompareOp,
- DepthBoundsTestEnable = DepthBoundsTestEnable,
+ DepthBoundsTestEnable = false,
StencilTestEnable = StencilTestEnable,
Front = stencilFront,
Back = stencilBack,
- MinDepthBounds = MinDepthBounds,
- MaxDepthBounds = MaxDepthBounds,
};
uint blendEnables = 0;
@@ -591,22 +564,21 @@ namespace Ryujinx.Graphics.Vulkan
}
bool supportsExtDynamicState = gd.Capabilities.SupportsExtendedDynamicState;
- int dynamicStatesCount = supportsExtDynamicState ? 9 : 8;
+ int dynamicStatesCount = supportsExtDynamicState ? 8 : 7;
DynamicState* dynamicStates = stackalloc DynamicState[dynamicStatesCount];
dynamicStates[0] = DynamicState.Viewport;
dynamicStates[1] = DynamicState.Scissor;
dynamicStates[2] = DynamicState.DepthBias;
- dynamicStates[3] = DynamicState.DepthBounds;
- dynamicStates[4] = DynamicState.StencilCompareMask;
- dynamicStates[5] = DynamicState.StencilWriteMask;
- dynamicStates[6] = DynamicState.StencilReference;
- dynamicStates[7] = DynamicState.BlendConstants;
+ dynamicStates[3] = DynamicState.StencilCompareMask;
+ dynamicStates[4] = DynamicState.StencilWriteMask;
+ dynamicStates[5] = DynamicState.StencilReference;
+ dynamicStates[6] = DynamicState.BlendConstants;
if (supportsExtDynamicState)
{
- dynamicStates[8] = DynamicState.VertexInputBindingStrideExt;
+ dynamicStates[7] = DynamicState.VertexInputBindingStrideExt;
}
var pipelineDynamicStateCreateInfo = new PipelineDynamicStateCreateInfo
@@ -632,7 +604,6 @@ namespace Ryujinx.Graphics.Vulkan
PDynamicState = &pipelineDynamicStateCreateInfo,
Layout = PipelineLayout,
RenderPass = renderPass,
- BasePipelineIndex = -1,
};
Result result = gd.Api.CreateGraphicsPipelines(device, cache, 1, &pipelineCreateInfo, null, &pipelineHandle);
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs b/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs
index 238f06e2a..c56224216 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineUid.cs
@@ -17,20 +17,17 @@ namespace Ryujinx.Graphics.Vulkan
public ulong Id4;
public ulong Id5;
public ulong Id6;
+
public ulong Id7;
-
public ulong Id8;
- public ulong Id9;
- private readonly uint VertexAttributeDescriptionsCount => (byte)((Id6 >> 38) & 0xFF);
- private readonly uint VertexBindingDescriptionsCount => (byte)((Id6 >> 46) & 0xFF);
- private readonly uint ColorBlendAttachmentStateCount => (byte)((Id7 >> 8) & 0xFF);
- private readonly bool HasDepthStencil => ((Id7 >> 63) & 0x1) != 0UL;
+ private readonly uint VertexAttributeDescriptionsCount => (byte)((Id5 >> 38) & 0xFF);
+ private readonly uint VertexBindingDescriptionsCount => (byte)((Id5 >> 46) & 0xFF);
+ private readonly uint ColorBlendAttachmentStateCount => (byte)((Id6 >> 8) & 0xFF);
+ private readonly bool HasDepthStencil => ((Id6 >> 63) & 0x1) != 0UL;
public Array32 VertexAttributeDescriptions;
public Array33 VertexBindingDescriptions;
- public Array16 Viewports;
- public Array16 Scissors;
public Array8 ColorBlendAttachmentState;
public Array9 AttachmentFormats;
public uint AttachmentIntegerFormatMask;
@@ -45,7 +42,7 @@ namespace Ryujinx.Graphics.Vulkan
{
if (!Unsafe.As>(ref Id0).Equals(Unsafe.As>(ref other.Id0)) ||
!Unsafe.As>(ref Id4).Equals(Unsafe.As>(ref other.Id4)) ||
- !Unsafe.As>(ref Id8).Equals(Unsafe.As>(ref other.Id8)))
+ !Unsafe.As>(ref Id7).Equals(Unsafe.As>(ref other.Id7)))
{
return false;
}
@@ -88,8 +85,7 @@ namespace Ryujinx.Graphics.Vulkan
Id5 * 23 ^
Id6 * 23 ^
Id7 * 23 ^
- Id8 * 23 ^
- Id9 * 23;
+ Id8 * 23;
for (int i = 0; i < (int)VertexAttributeDescriptionsCount; i++)
{
From c0f2491eaee7eb1088605f5bda8055b941a14f99 Mon Sep 17 00:00:00 2001
From: gdkchan
Date: Sun, 2 Jun 2024 22:40:28 -0300
Subject: [PATCH 037/109] Vulkan separate descriptor set fixes (#6895)
* Ensure descriptor sets are only re-used when all command buffers using it have completed
* Fix some SPIR-V capabilities
* Set update after bind flag if we exceed limits
* Simpler fix for Intel
* Format whitespace
* Make struct readonly
* Add barriers for extra set arrays too
---
src/Ryujinx.Graphics.GAL/IImageArray.cs | 4 +-
src/Ryujinx.Graphics.GAL/ITextureArray.cs | 4 +-
.../Multithreading/CommandHelper.cs | 2 +
.../Multithreading/CommandType.cs | 2 +
.../ImageArray/ImageArrayDisposeCommand.cs | 21 +++
.../TextureArrayDisposeCommand.cs | 21 +++
.../TextureArraySetSamplersCommand.cs | 0
.../TextureArraySetTexturesCommand.cs | 0
.../Resources/ThreadedImageArray.cs | 6 +
.../Resources/ThreadedTextureArray.cs | 6 +
.../Image/TextureBindingsArrayCache.cs | 20 ++-
.../Image/ImageArray.cs | 4 +
.../Image/TextureArray.cs | 4 +
.../CodeGen/Spirv/CodeGenContext.cs | 5 -
.../CodeGen/Spirv/SpirvGenerator.cs | 12 +-
.../Translation/HostCapabilities.cs | 3 +
.../Translation/TranslatorContext.cs | 1 +
.../DescriptorSetUpdater.cs | 41 +++++-
src/Ryujinx.Graphics.Vulkan/ImageArray.cs | 40 +-----
.../PipelineLayoutCacheEntry.cs | 124 ++++++++++++++----
.../PipelineLayoutFactory.cs | 35 ++++-
src/Ryujinx.Graphics.Vulkan/ResourceArray.cs | 74 +++++++++++
.../ShaderCollection.cs | 9 +-
src/Ryujinx.Graphics.Vulkan/TextureArray.cs | 40 +-----
24 files changed, 365 insertions(+), 113 deletions(-)
create mode 100644 src/Ryujinx.Graphics.GAL/Multithreading/Commands/ImageArray/ImageArrayDisposeCommand.cs
create mode 100644 src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureArray/TextureArrayDisposeCommand.cs
rename src/Ryujinx.Graphics.GAL/Multithreading/Commands/{TextureAndSamplerArray => TextureArray}/TextureArraySetSamplersCommand.cs (100%)
rename src/Ryujinx.Graphics.GAL/Multithreading/Commands/{TextureAndSamplerArray => TextureArray}/TextureArraySetTexturesCommand.cs (100%)
create mode 100644 src/Ryujinx.Graphics.Vulkan/ResourceArray.cs
diff --git a/src/Ryujinx.Graphics.GAL/IImageArray.cs b/src/Ryujinx.Graphics.GAL/IImageArray.cs
index 30cff50b1..d119aa9fb 100644
--- a/src/Ryujinx.Graphics.GAL/IImageArray.cs
+++ b/src/Ryujinx.Graphics.GAL/IImageArray.cs
@@ -1,6 +1,8 @@
+using System;
+
namespace Ryujinx.Graphics.GAL
{
- public interface IImageArray
+ public interface IImageArray : IDisposable
{
void SetFormats(int index, Format[] imageFormats);
void SetImages(int index, ITexture[] images);
diff --git a/src/Ryujinx.Graphics.GAL/ITextureArray.cs b/src/Ryujinx.Graphics.GAL/ITextureArray.cs
index 35c2116b5..9ee79dacb 100644
--- a/src/Ryujinx.Graphics.GAL/ITextureArray.cs
+++ b/src/Ryujinx.Graphics.GAL/ITextureArray.cs
@@ -1,6 +1,8 @@
+using System;
+
namespace Ryujinx.Graphics.GAL
{
- public interface ITextureArray
+ public interface ITextureArray : IDisposable
{
void SetSamplers(int index, ISampler[] samplers);
void SetTextures(int index, ITexture[] textures);
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs b/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
index edaae3042..ef227d4a5 100644
--- a/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/CommandHelper.cs
@@ -66,6 +66,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
Register(CommandType.CounterEventDispose);
Register(CommandType.CounterEventFlush);
+ Register(CommandType.ImageArrayDispose);
Register(CommandType.ImageArraySetFormats);
Register(CommandType.ImageArraySetImages);
@@ -88,6 +89,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
Register(CommandType.TextureSetDataSliceRegion);
Register(CommandType.TextureSetStorage);
+ Register(CommandType.TextureArrayDispose);
Register(CommandType.TextureArraySetSamplers);
Register(CommandType.TextureArraySetTextures);
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs b/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs
index 758695352..cf3f5d6c1 100644
--- a/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/CommandType.cs
@@ -26,6 +26,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
CounterEventDispose,
CounterEventFlush,
+ ImageArrayDispose,
ImageArraySetFormats,
ImageArraySetImages,
@@ -48,6 +49,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
TextureSetDataSliceRegion,
TextureSetStorage,
+ TextureArrayDispose,
TextureArraySetSamplers,
TextureArraySetTextures,
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/ImageArray/ImageArrayDisposeCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/ImageArray/ImageArrayDisposeCommand.cs
new file mode 100644
index 000000000..ac2ac933b
--- /dev/null
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/ImageArray/ImageArrayDisposeCommand.cs
@@ -0,0 +1,21 @@
+using Ryujinx.Graphics.GAL.Multithreading.Model;
+using Ryujinx.Graphics.GAL.Multithreading.Resources;
+
+namespace Ryujinx.Graphics.GAL.Multithreading.Commands.ImageArray
+{
+ struct ImageArrayDisposeCommand : IGALCommand, IGALCommand
+ {
+ public readonly CommandType CommandType => CommandType.ImageArrayDispose;
+ private TableRef _imageArray;
+
+ public void Set(TableRef imageArray)
+ {
+ _imageArray = imageArray;
+ }
+
+ public static void Run(ref ImageArrayDisposeCommand command, ThreadedRenderer threaded, IRenderer renderer)
+ {
+ command._imageArray.Get(threaded).Base.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureArray/TextureArrayDisposeCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureArray/TextureArrayDisposeCommand.cs
new file mode 100644
index 000000000..fec1c48f0
--- /dev/null
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureArray/TextureArrayDisposeCommand.cs
@@ -0,0 +1,21 @@
+using Ryujinx.Graphics.GAL.Multithreading.Model;
+using Ryujinx.Graphics.GAL.Multithreading.Resources;
+
+namespace Ryujinx.Graphics.GAL.Multithreading.Commands.TextureArray
+{
+ struct TextureArrayDisposeCommand : IGALCommand, IGALCommand
+ {
+ public readonly CommandType CommandType => CommandType.TextureArrayDispose;
+ private TableRef _textureArray;
+
+ public void Set(TableRef textureArray)
+ {
+ _textureArray = textureArray;
+ }
+
+ public static void Run(ref TextureArrayDisposeCommand command, ThreadedRenderer threaded, IRenderer renderer)
+ {
+ command._textureArray.Get(threaded).Base.Dispose();
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureAndSamplerArray/TextureArraySetSamplersCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureArray/TextureArraySetSamplersCommand.cs
similarity index 100%
rename from src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureAndSamplerArray/TextureArraySetSamplersCommand.cs
rename to src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureArray/TextureArraySetSamplersCommand.cs
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureAndSamplerArray/TextureArraySetTexturesCommand.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureArray/TextureArraySetTexturesCommand.cs
similarity index 100%
rename from src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureAndSamplerArray/TextureArraySetTexturesCommand.cs
rename to src/Ryujinx.Graphics.GAL/Multithreading/Commands/TextureArray/TextureArraySetTexturesCommand.cs
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedImageArray.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedImageArray.cs
index d26ee1fbd..19bc6f233 100644
--- a/src/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedImageArray.cs
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedImageArray.cs
@@ -21,6 +21,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Resources
return new TableRef(_renderer, reference);
}
+ public void Dispose()
+ {
+ _renderer.New().Set(Ref(this));
+ _renderer.QueueCommand();
+ }
+
public void SetFormats(int index, Format[] imageFormats)
{
_renderer.New().Set(Ref(this), index, Ref(imageFormats));
diff --git a/src/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedTextureArray.cs b/src/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedTextureArray.cs
index 82405a1f6..4334c7048 100644
--- a/src/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedTextureArray.cs
+++ b/src/Ryujinx.Graphics.GAL/Multithreading/Resources/ThreadedTextureArray.cs
@@ -22,6 +22,12 @@ namespace Ryujinx.Graphics.GAL.Multithreading.Resources
return new TableRef(_renderer, reference);
}
+ public void Dispose()
+ {
+ _renderer.New().Set(Ref(this));
+ _renderer.QueueCommand();
+ }
+
public void SetSamplers(int index, ISampler[] samplers)
{
_renderer.New().Set(Ref(this), index, Ref(samplers.ToArray()));
diff --git a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs
index 18e28b3dd..01e34c777 100644
--- a/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs
+++ b/src/Ryujinx.Graphics.Gpu/Image/TextureBindingsArrayCache.cs
@@ -1113,6 +1113,15 @@ namespace Ryujinx.Graphics.Gpu.Image
nextNode = nextNode.Next;
_cacheFromBuffer.Remove(toRemove.Value.Key);
_lruCache.Remove(toRemove);
+
+ if (toRemove.Value.Key.IsImage)
+ {
+ toRemove.Value.ImageArray.Dispose();
+ }
+ else
+ {
+ toRemove.Value.TextureArray.Dispose();
+ }
}
}
@@ -1124,11 +1133,20 @@ namespace Ryujinx.Graphics.Gpu.Image
{
List keysToRemove = null;
- foreach (CacheEntryFromPoolKey key in _cacheFromPool.Keys)
+ foreach ((CacheEntryFromPoolKey key, CacheEntry entry) in _cacheFromPool)
{
if (key.MatchesPool(pool))
{
(keysToRemove ??= new()).Add(key);
+
+ if (key.IsImage)
+ {
+ entry.ImageArray.Dispose();
+ }
+ else
+ {
+ entry.TextureArray.Dispose();
+ }
}
}
diff --git a/src/Ryujinx.Graphics.OpenGL/Image/ImageArray.cs b/src/Ryujinx.Graphics.OpenGL/Image/ImageArray.cs
index 1c5acedf3..6198823d9 100644
--- a/src/Ryujinx.Graphics.OpenGL/Image/ImageArray.cs
+++ b/src/Ryujinx.Graphics.OpenGL/Image/ImageArray.cs
@@ -63,5 +63,9 @@ namespace Ryujinx.Graphics.OpenGL.Image
}
}
}
+
+ public void Dispose()
+ {
+ }
}
}
diff --git a/src/Ryujinx.Graphics.OpenGL/Image/TextureArray.cs b/src/Ryujinx.Graphics.OpenGL/Image/TextureArray.cs
index d70b0a008..41ac058c1 100644
--- a/src/Ryujinx.Graphics.OpenGL/Image/TextureArray.cs
+++ b/src/Ryujinx.Graphics.OpenGL/Image/TextureArray.cs
@@ -48,5 +48,9 @@ namespace Ryujinx.Graphics.OpenGL.Image
}
}
}
+
+ public void Dispose()
+ {
+ }
}
}
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
index f3be29bb9..cc7977f84 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/CodeGenContext.cs
@@ -98,11 +98,6 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
Logger = parameters.Logger;
TargetApi = parameters.TargetApi;
- AddCapability(Capability.Shader);
- AddCapability(Capability.Float64);
-
- SetMemoryModel(AddressingModel.Logical, MemoryModel.GLSL450);
-
Delegates = new SpirvDelegates(this);
}
diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs
index ccfdc46d0..b259dde28 100644
--- a/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs
+++ b/src/Ryujinx.Graphics.Shader/CodeGen/Spirv/SpirvGenerator.cs
@@ -43,6 +43,10 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
CodeGenContext context = new(info, parameters, instPool, integerPool);
+ context.AddCapability(Capability.Shader);
+
+ context.SetMemoryModel(AddressingModel.Logical, MemoryModel.GLSL450);
+
context.AddCapability(Capability.GroupNonUniformBallot);
context.AddCapability(Capability.GroupNonUniformShuffle);
context.AddCapability(Capability.GroupNonUniformVote);
@@ -51,6 +55,11 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
context.AddCapability(Capability.ImageQuery);
context.AddCapability(Capability.SampledBuffer);
+ if (parameters.HostCapabilities.SupportsShaderFloat64)
+ {
+ context.AddCapability(Capability.Float64);
+ }
+
if (parameters.Definitions.TransformFeedbackEnabled && parameters.Definitions.LastInVertexPipeline)
{
context.AddCapability(Capability.TransformFeedback);
@@ -58,7 +67,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
if (parameters.Definitions.Stage == ShaderStage.Fragment)
{
- if (context.Info.IoDefinitions.Contains(new IoDefinition(StorageKind.Input, IoVariable.Layer)))
+ if (context.Info.IoDefinitions.Contains(new IoDefinition(StorageKind.Input, IoVariable.Layer)) ||
+ context.Info.IoDefinitions.Contains(new IoDefinition(StorageKind.Input, IoVariable.PrimitiveId)))
{
context.AddCapability(Capability.Geometry);
}
diff --git a/src/Ryujinx.Graphics.Shader/Translation/HostCapabilities.cs b/src/Ryujinx.Graphics.Shader/Translation/HostCapabilities.cs
index 2523272b0..11fe6599d 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/HostCapabilities.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/HostCapabilities.cs
@@ -8,6 +8,7 @@ namespace Ryujinx.Graphics.Shader.Translation
public readonly bool SupportsGeometryShaderPassthrough;
public readonly bool SupportsShaderBallot;
public readonly bool SupportsShaderBarrierDivergence;
+ public readonly bool SupportsShaderFloat64;
public readonly bool SupportsTextureShadowLod;
public readonly bool SupportsViewportMask;
@@ -18,6 +19,7 @@ namespace Ryujinx.Graphics.Shader.Translation
bool supportsGeometryShaderPassthrough,
bool supportsShaderBallot,
bool supportsShaderBarrierDivergence,
+ bool supportsShaderFloat64,
bool supportsTextureShadowLod,
bool supportsViewportMask)
{
@@ -27,6 +29,7 @@ namespace Ryujinx.Graphics.Shader.Translation
SupportsGeometryShaderPassthrough = supportsGeometryShaderPassthrough;
SupportsShaderBallot = supportsShaderBallot;
SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence;
+ SupportsShaderFloat64 = supportsShaderFloat64;
SupportsTextureShadowLod = supportsTextureShadowLod;
SupportsViewportMask = supportsViewportMask;
}
diff --git a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
index 59914736e..a579433f9 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs
@@ -363,6 +363,7 @@ namespace Ryujinx.Graphics.Shader.Translation
GpuAccessor.QueryHostSupportsGeometryShaderPassthrough(),
GpuAccessor.QueryHostSupportsShaderBallot(),
GpuAccessor.QueryHostSupportsShaderBarrierDivergence(),
+ GpuAccessor.QueryHostSupportsShaderFloat64(),
GpuAccessor.QueryHostSupportsTextureShadowLod(),
GpuAccessor.QueryHostSupportsViewportMask());
diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs
index 382f88d05..3590d5d05 100644
--- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs
+++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs
@@ -291,8 +291,9 @@ namespace Ryujinx.Graphics.Vulkan
}
else
{
- PipelineStageFlags stageFlags = _textureArrayRefs[segment.Binding].Stage.ConvertToPipelineStageFlags();
- _textureArrayRefs[segment.Binding].Array?.QueueWriteToReadBarriers(cbs, stageFlags);
+ ref var arrayRef = ref _textureArrayRefs[segment.Binding];
+ PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags();
+ arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags);
}
}
}
@@ -311,8 +312,40 @@ namespace Ryujinx.Graphics.Vulkan
}
else
{
- PipelineStageFlags stageFlags = _imageArrayRefs[segment.Binding].Stage.ConvertToPipelineStageFlags();
- _imageArrayRefs[segment.Binding].Array?.QueueWriteToReadBarriers(cbs, stageFlags);
+ ref var arrayRef = ref _imageArrayRefs[segment.Binding];
+ PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags();
+ arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags);
+ }
+ }
+ }
+
+ for (int setIndex = PipelineBase.DescriptorSetLayouts; setIndex < _program.BindingSegments.Length; setIndex++)
+ {
+ var bindingSegments = _program.BindingSegments[setIndex];
+
+ if (bindingSegments.Length == 0)
+ {
+ continue;
+ }
+
+ ResourceBindingSegment segment = bindingSegments[0];
+
+ if (segment.IsArray)
+ {
+ if (segment.Type == ResourceType.Texture ||
+ segment.Type == ResourceType.Sampler ||
+ segment.Type == ResourceType.TextureAndSampler ||
+ segment.Type == ResourceType.BufferTexture)
+ {
+ ref var arrayRef = ref _textureArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts];
+ PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags();
+ arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags);
+ }
+ else if (segment.Type == ResourceType.Image || segment.Type == ResourceType.BufferImage)
+ {
+ ref var arrayRef = ref _imageArrayExtraRefs[setIndex - PipelineBase.DescriptorSetLayouts];
+ PipelineStageFlags stageFlags = arrayRef.Stage.ConvertToPipelineStageFlags();
+ arrayRef.Array?.QueueWriteToReadBarriers(cbs, stageFlags);
}
}
}
diff --git a/src/Ryujinx.Graphics.Vulkan/ImageArray.cs b/src/Ryujinx.Graphics.Vulkan/ImageArray.cs
index 3c7f321ff..e42750d3c 100644
--- a/src/Ryujinx.Graphics.Vulkan/ImageArray.cs
+++ b/src/Ryujinx.Graphics.Vulkan/ImageArray.cs
@@ -2,11 +2,10 @@ using Ryujinx.Graphics.GAL;
using Silk.NET.Vulkan;
using System;
using System.Collections.Generic;
-using System.Diagnostics;
namespace Ryujinx.Graphics.Vulkan
{
- class ImageArray : IImageArray
+ class ImageArray : ResourceArray, IImageArray
{
private readonly VulkanRenderer _gd;
@@ -25,19 +24,11 @@ namespace Ryujinx.Graphics.Vulkan
private HashSet _storages;
- private DescriptorSet[] _cachedDescriptorSets;
-
private int _cachedCommandBufferIndex;
private int _cachedSubmissionCount;
- private ShaderCollection _cachedDscProgram;
- private int _cachedDscSetIndex;
- private int _cachedDscIndex;
-
private readonly bool _isBuffer;
- private int _bindCount;
-
public ImageArray(VulkanRenderer gd, int size, bool isBuffer)
{
_gd = gd;
@@ -104,12 +95,7 @@ namespace Ryujinx.Graphics.Vulkan
{
_cachedCommandBufferIndex = -1;
_storages = null;
- _cachedDescriptorSets = null;
-
- if (_bindCount != 0)
- {
- _gd.PipelineInternal.ForceImageDirty();
- }
+ SetDirty(_gd);
}
public void QueueWriteToReadBarriers(CommandBufferScoped cbs, PipelineStageFlags stageFlags)
@@ -195,7 +181,7 @@ namespace Ryujinx.Graphics.Vulkan
int setIndex,
TextureView dummyTexture)
{
- if (_cachedDescriptorSets != null)
+ if (TryGetCachedDescriptorSets(cbs, program, setIndex, out DescriptorSet[] sets))
{
// We still need to ensure the current command buffer holds a reference to all used textures.
@@ -208,12 +194,9 @@ namespace Ryujinx.Graphics.Vulkan
GetBufferViews(cbs);
}
- return _cachedDescriptorSets;
+ return sets;
}
- _cachedDscProgram?.ReleaseManualDescriptorSetCollection(_cachedDscSetIndex, _cachedDscIndex);
- var dsc = program.GetNewManualDescriptorSetCollection(cbs.CommandBufferIndex, setIndex, out _cachedDscIndex).Get(cbs);
-
DescriptorSetTemplate template = program.Templates[setIndex];
DescriptorSetTemplateWriter tu = templateUpdater.Begin(template);
@@ -227,24 +210,9 @@ namespace Ryujinx.Graphics.Vulkan
tu.Push(GetBufferViews(cbs));
}
- var sets = dsc.GetSets();
templateUpdater.Commit(_gd, device, sets[0]);
- _cachedDescriptorSets = sets;
- _cachedDscProgram = program;
- _cachedDscSetIndex = setIndex;
return sets;
}
-
- public void IncrementBindCount()
- {
- _bindCount++;
- }
-
- public void DecrementBindCount()
- {
- int newBindCount = --_bindCount;
- Debug.Assert(newBindCount >= 0);
- }
}
}
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs
index 7d0948d6e..ae296b033 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutCacheEntry.cs
@@ -3,6 +3,7 @@ using Silk.NET.Vulkan;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Diagnostics;
using System.Runtime.InteropServices;
namespace Ryujinx.Graphics.Vulkan
@@ -15,6 +16,7 @@ namespace Ryujinx.Graphics.Vulkan
private readonly Device _device;
public DescriptorSetLayout[] DescriptorSetLayouts { get; }
+ public bool[] DescriptorSetLayoutsUpdateAfterBind { get; }
public PipelineLayout PipelineLayout { get; }
private readonly int[] _consumedDescriptorsPerSet;
@@ -31,20 +33,37 @@ namespace Ryujinx.Graphics.Vulkan
private struct ManualDescriptorSetEntry
{
public Auto DescriptorSet;
- public int CbIndex;
- public int CbSubmissionCount;
+ public uint CbRefMask;
public bool InUse;
- public ManualDescriptorSetEntry(Auto descriptorSet, int cbIndex, int cbSubmissionCount, bool inUse)
+ public ManualDescriptorSetEntry(Auto descriptorSet, int cbIndex)
{
DescriptorSet = descriptorSet;
- CbIndex = cbIndex;
- CbSubmissionCount = cbSubmissionCount;
- InUse = inUse;
+ CbRefMask = 1u << cbIndex;
+ InUse = true;
+ }
+ }
+
+ private readonly struct PendingManualDsConsumption
+ {
+ public FenceHolder Fence { get; }
+ public int CommandBufferIndex { get; }
+ public int SetIndex { get; }
+ public int CacheIndex { get; }
+
+ public PendingManualDsConsumption(FenceHolder fence, int commandBufferIndex, int setIndex, int cacheIndex)
+ {
+ Fence = fence;
+ CommandBufferIndex = commandBufferIndex;
+ SetIndex = setIndex;
+ CacheIndex = cacheIndex;
+ fence.Get();
}
}
private readonly List[] _manualDsCache;
+ private readonly Queue _pendingManualDsConsumptions;
+ private readonly Queue[] _freeManualDsCacheEntries;
private readonly Dictionary _pdTemplates;
private readonly ResourceDescriptorCollection _pdDescriptors;
@@ -70,6 +89,8 @@ namespace Ryujinx.Graphics.Vulkan
_dsCacheCursor = new int[setsCount];
_manualDsCache = new List[setsCount];
+ _pendingManualDsConsumptions = new Queue();
+ _freeManualDsCacheEntries = new Queue[setsCount];
}
public PipelineLayoutCacheEntry(
@@ -78,7 +99,11 @@ namespace Ryujinx.Graphics.Vulkan
ReadOnlyCollection setDescriptors,
bool usePushDescriptors) : this(gd, device, setDescriptors.Count)
{
- (DescriptorSetLayouts, PipelineLayout) = PipelineLayoutFactory.Create(gd, device, setDescriptors, usePushDescriptors);
+ ResourceLayouts layouts = PipelineLayoutFactory.Create(gd, device, setDescriptors, usePushDescriptors);
+
+ DescriptorSetLayouts = layouts.DescriptorSetLayouts;
+ DescriptorSetLayoutsUpdateAfterBind = layouts.DescriptorSetLayoutsUpdateAfterBind;
+ PipelineLayout = layouts.PipelineLayout;
_consumedDescriptorsPerSet = new int[setDescriptors.Count];
_poolSizes = new DescriptorPoolSize[setDescriptors.Count][];
@@ -133,7 +158,7 @@ namespace Ryujinx.Graphics.Vulkan
_poolSizes[setIndex],
setIndex,
_consumedDescriptorsPerSet[setIndex],
- false);
+ DescriptorSetLayoutsUpdateAfterBind[setIndex]);
list.Add(dsc);
isNew = true;
@@ -144,49 +169,99 @@ namespace Ryujinx.Graphics.Vulkan
return list[index];
}
- public Auto GetNewManualDescriptorSetCollection(int commandBufferIndex, int setIndex, out int cacheIndex)
+ public Auto GetNewManualDescriptorSetCollection(CommandBufferScoped cbs, int setIndex, out int cacheIndex)
{
- int submissionCount = _gd.CommandBufferPool.GetSubmissionCount(commandBufferIndex);
+ FreeCompletedManualDescriptorSets();
var list = _manualDsCache[setIndex] ??= new();
var span = CollectionsMarshal.AsSpan(list);
- for (int index = 0; index < span.Length; index++)
+ Queue freeQueue = _freeManualDsCacheEntries[setIndex];
+
+ // Do we have at least one freed descriptor set? If so, just use that.
+ if (freeQueue != null && freeQueue.TryDequeue(out int freeIndex))
{
- ref ManualDescriptorSetEntry entry = ref span[index];
+ ref ManualDescriptorSetEntry entry = ref span[freeIndex];
- if (!entry.InUse && (entry.CbIndex != commandBufferIndex || entry.CbSubmissionCount != submissionCount))
- {
- entry.InUse = true;
- entry.CbIndex = commandBufferIndex;
- entry.CbSubmissionCount = submissionCount;
+ Debug.Assert(!entry.InUse && entry.CbRefMask == 0);
- cacheIndex = index;
+ entry.InUse = true;
+ entry.CbRefMask = 1u << cbs.CommandBufferIndex;
+ cacheIndex = freeIndex;
- return entry.DescriptorSet;
- }
+ _pendingManualDsConsumptions.Enqueue(new PendingManualDsConsumption(cbs.GetFence(), cbs.CommandBufferIndex, setIndex, freeIndex));
+
+ return entry.DescriptorSet;
}
+ // Otherwise create a new descriptor set, and add to our pending queue for command buffer consumption tracking.
var dsc = _descriptorSetManager.AllocateDescriptorSet(
_gd.Api,
DescriptorSetLayouts[setIndex],
_poolSizes[setIndex],
setIndex,
_consumedDescriptorsPerSet[setIndex],
- false);
+ DescriptorSetLayoutsUpdateAfterBind[setIndex]);
cacheIndex = list.Count;
- list.Add(new ManualDescriptorSetEntry(dsc, commandBufferIndex, submissionCount, inUse: true));
+ list.Add(new ManualDescriptorSetEntry(dsc, cbs.CommandBufferIndex));
+ _pendingManualDsConsumptions.Enqueue(new PendingManualDsConsumption(cbs.GetFence(), cbs.CommandBufferIndex, setIndex, cacheIndex));
return dsc;
}
+ public void UpdateManualDescriptorSetCollectionOwnership(CommandBufferScoped cbs, int setIndex, int cacheIndex)
+ {
+ FreeCompletedManualDescriptorSets();
+
+ var list = _manualDsCache[setIndex];
+ var span = CollectionsMarshal.AsSpan(list);
+ ref var entry = ref span[cacheIndex];
+
+ uint cbMask = 1u << cbs.CommandBufferIndex;
+
+ if ((entry.CbRefMask & cbMask) == 0)
+ {
+ entry.CbRefMask |= cbMask;
+
+ _pendingManualDsConsumptions.Enqueue(new PendingManualDsConsumption(cbs.GetFence(), cbs.CommandBufferIndex, setIndex, cacheIndex));
+ }
+ }
+
+ private void FreeCompletedManualDescriptorSets()
+ {
+ FenceHolder signalledFence = null;
+ while (_pendingManualDsConsumptions.TryPeek(out var pds) && (pds.Fence == signalledFence || pds.Fence.IsSignaled()))
+ {
+ signalledFence = pds.Fence; // Already checked - don't need to do it again.
+ var dequeued = _pendingManualDsConsumptions.Dequeue();
+ Debug.Assert(dequeued.Fence == pds.Fence);
+ pds.Fence.Put();
+
+ var span = CollectionsMarshal.AsSpan(_manualDsCache[dequeued.SetIndex]);
+ ref var entry = ref span[dequeued.CacheIndex];
+ entry.CbRefMask &= ~(1u << dequeued.CommandBufferIndex);
+
+ if (!entry.InUse && entry.CbRefMask == 0)
+ {
+ // If not in use by any array, and not bound to any command buffer, the descriptor set can be re-used immediately.
+ (_freeManualDsCacheEntries[dequeued.SetIndex] ??= new()).Enqueue(dequeued.CacheIndex);
+ }
+ }
+ }
+
public void ReleaseManualDescriptorSetCollection(int setIndex, int cacheIndex)
{
var list = _manualDsCache[setIndex];
var span = CollectionsMarshal.AsSpan(list);
span[cacheIndex].InUse = false;
+
+ if (span[cacheIndex].CbRefMask == 0)
+ {
+ // This is no longer in use by any array, so if not bound to any command buffer, the descriptor set can be re-used immediately.
+ (_freeManualDsCacheEntries[setIndex] ??= new()).Enqueue(cacheIndex);
+ }
}
private static Span GetDescriptorPoolSizes(Span output, ResourceDescriptorCollection setDescriptor, uint multiplier)
@@ -291,6 +366,11 @@ namespace Ryujinx.Graphics.Vulkan
_gd.Api.DestroyDescriptorSetLayout(_device, DescriptorSetLayouts[i], null);
}
+ while (_pendingManualDsConsumptions.TryDequeue(out var pds))
+ {
+ pds.Fence.Put();
+ }
+
_descriptorSetManager.Dispose();
}
}
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs
index 8bf286c65..bca119f6a 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineLayoutFactory.cs
@@ -1,18 +1,23 @@
+using Ryujinx.Common.Memory;
using Ryujinx.Graphics.GAL;
using Silk.NET.Vulkan;
+using System;
using System.Collections.ObjectModel;
namespace Ryujinx.Graphics.Vulkan
{
+ record struct ResourceLayouts(DescriptorSetLayout[] DescriptorSetLayouts, bool[] DescriptorSetLayoutsUpdateAfterBind, PipelineLayout PipelineLayout);
+
static class PipelineLayoutFactory
{
- public static unsafe (DescriptorSetLayout[], PipelineLayout) Create(
+ public static unsafe ResourceLayouts Create(
VulkanRenderer gd,
Device device,
ReadOnlyCollection setDescriptors,
bool usePushDescriptors)
{
DescriptorSetLayout[] layouts = new DescriptorSetLayout[setDescriptors.Count];
+ bool[] updateAfterBindFlags = new bool[setDescriptors.Count];
bool isMoltenVk = gd.IsMoltenVk;
@@ -32,10 +37,11 @@ namespace Ryujinx.Graphics.Vulkan
DescriptorSetLayoutBinding[] layoutBindings = new DescriptorSetLayoutBinding[rdc.Descriptors.Count];
+ bool hasArray = false;
+
for (int descIndex = 0; descIndex < rdc.Descriptors.Count; descIndex++)
{
ResourceDescriptor descriptor = rdc.Descriptors[descIndex];
-
ResourceStages stages = descriptor.Stages;
if (descriptor.Type == ResourceType.StorageBuffer && isMoltenVk)
@@ -52,16 +58,37 @@ namespace Ryujinx.Graphics.Vulkan
DescriptorCount = (uint)descriptor.Count,
StageFlags = stages.Convert(),
};
+
+ if (descriptor.Count > 1)
+ {
+ hasArray = true;
+ }
}
fixed (DescriptorSetLayoutBinding* pLayoutBindings = layoutBindings)
{
+ DescriptorSetLayoutCreateFlags flags = DescriptorSetLayoutCreateFlags.None;
+
+ if (usePushDescriptors && setIndex == 0)
+ {
+ flags = DescriptorSetLayoutCreateFlags.PushDescriptorBitKhr;
+ }
+
+ if (gd.Vendor == Vendor.Intel && hasArray)
+ {
+ // Some vendors (like Intel) have low per-stage limits.
+ // We must set the flag if we exceed those limits.
+ flags |= DescriptorSetLayoutCreateFlags.UpdateAfterBindPoolBit;
+
+ updateAfterBindFlags[setIndex] = true;
+ }
+
var descriptorSetLayoutCreateInfo = new DescriptorSetLayoutCreateInfo
{
SType = StructureType.DescriptorSetLayoutCreateInfo,
PBindings = pLayoutBindings,
BindingCount = (uint)layoutBindings.Length,
- Flags = usePushDescriptors && setIndex == 0 ? DescriptorSetLayoutCreateFlags.PushDescriptorBitKhr : DescriptorSetLayoutCreateFlags.None,
+ Flags = flags,
};
gd.Api.CreateDescriptorSetLayout(device, descriptorSetLayoutCreateInfo, null, out layouts[setIndex]).ThrowOnError();
@@ -82,7 +109,7 @@ namespace Ryujinx.Graphics.Vulkan
gd.Api.CreatePipelineLayout(device, &pipelineLayoutCreateInfo, null, out layout).ThrowOnError();
}
- return (layouts, layout);
+ return new ResourceLayouts(layouts, updateAfterBindFlags, layout);
}
}
}
diff --git a/src/Ryujinx.Graphics.Vulkan/ResourceArray.cs b/src/Ryujinx.Graphics.Vulkan/ResourceArray.cs
new file mode 100644
index 000000000..0880a10f0
--- /dev/null
+++ b/src/Ryujinx.Graphics.Vulkan/ResourceArray.cs
@@ -0,0 +1,74 @@
+using Silk.NET.Vulkan;
+using System;
+using System.Diagnostics;
+
+namespace Ryujinx.Graphics.Vulkan
+{
+ class ResourceArray : IDisposable
+ {
+ private DescriptorSet[] _cachedDescriptorSets;
+
+ private ShaderCollection _cachedDscProgram;
+ private int _cachedDscSetIndex;
+ private int _cachedDscIndex;
+
+ private int _bindCount;
+
+ protected void SetDirty(VulkanRenderer gd)
+ {
+ ReleaseDescriptorSet();
+
+ if (_bindCount != 0)
+ {
+ gd.PipelineInternal.ForceTextureDirty();
+ }
+ }
+
+ public bool TryGetCachedDescriptorSets(CommandBufferScoped cbs, ShaderCollection program, int setIndex, out DescriptorSet[] sets)
+ {
+ if (_cachedDescriptorSets != null)
+ {
+ _cachedDscProgram.UpdateManualDescriptorSetCollectionOwnership(cbs, _cachedDscSetIndex, _cachedDscIndex);
+
+ sets = _cachedDescriptorSets;
+
+ return true;
+ }
+
+ var dsc = program.GetNewManualDescriptorSetCollection(cbs, setIndex, out _cachedDscIndex).Get(cbs);
+
+ sets = dsc.GetSets();
+
+ _cachedDescriptorSets = sets;
+ _cachedDscProgram = program;
+ _cachedDscSetIndex = setIndex;
+
+ return false;
+ }
+
+ public void IncrementBindCount()
+ {
+ _bindCount++;
+ }
+
+ public void DecrementBindCount()
+ {
+ int newBindCount = --_bindCount;
+ Debug.Assert(newBindCount >= 0);
+ }
+
+ private void ReleaseDescriptorSet()
+ {
+ if (_cachedDescriptorSets != null)
+ {
+ _cachedDscProgram.ReleaseManualDescriptorSetCollection(_cachedDscSetIndex, _cachedDscIndex);
+ _cachedDescriptorSets = null;
+ }
+ }
+
+ public void Dispose()
+ {
+ ReleaseDescriptorSet();
+ }
+ }
+}
diff --git a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs
index f2d648a51..f9637789e 100644
--- a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs
+++ b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs
@@ -604,9 +604,14 @@ namespace Ryujinx.Graphics.Vulkan
return _plce.GetNewDescriptorSetCollection(setIndex, out isNew);
}
- public Auto GetNewManualDescriptorSetCollection(int commandBufferIndex, int setIndex, out int cacheIndex)
+ public Auto GetNewManualDescriptorSetCollection(CommandBufferScoped cbs, int setIndex, out int cacheIndex)
{
- return _plce.GetNewManualDescriptorSetCollection(commandBufferIndex, setIndex, out cacheIndex);
+ return _plce.GetNewManualDescriptorSetCollection(cbs, setIndex, out cacheIndex);
+ }
+
+ public void UpdateManualDescriptorSetCollectionOwnership(CommandBufferScoped cbs, int setIndex, int cacheIndex)
+ {
+ _plce.UpdateManualDescriptorSetCollectionOwnership(cbs, setIndex, cacheIndex);
}
public void ReleaseManualDescriptorSetCollection(int setIndex, int cacheIndex)
diff --git a/src/Ryujinx.Graphics.Vulkan/TextureArray.cs b/src/Ryujinx.Graphics.Vulkan/TextureArray.cs
index fe834225c..31c408d64 100644
--- a/src/Ryujinx.Graphics.Vulkan/TextureArray.cs
+++ b/src/Ryujinx.Graphics.Vulkan/TextureArray.cs
@@ -2,11 +2,10 @@ using Ryujinx.Graphics.GAL;
using Silk.NET.Vulkan;
using System;
using System.Collections.Generic;
-using System.Diagnostics;
namespace Ryujinx.Graphics.Vulkan
{
- class TextureArray : ITextureArray
+ class TextureArray : ResourceArray, ITextureArray
{
private readonly VulkanRenderer _gd;
@@ -25,19 +24,11 @@ namespace Ryujinx.Graphics.Vulkan
private HashSet _storages;
- private DescriptorSet[] _cachedDescriptorSets;
-
private int _cachedCommandBufferIndex;
private int _cachedSubmissionCount;
- private ShaderCollection _cachedDscProgram;
- private int _cachedDscSetIndex;
- private int _cachedDscIndex;
-
private readonly bool _isBuffer;
- private int _bindCount;
-
public TextureArray(VulkanRenderer gd, int size, bool isBuffer)
{
_gd = gd;
@@ -113,12 +104,7 @@ namespace Ryujinx.Graphics.Vulkan
{
_cachedCommandBufferIndex = -1;
_storages = null;
- _cachedDescriptorSets = null;
-
- if (_bindCount != 0)
- {
- _gd.PipelineInternal.ForceTextureDirty();
- }
+ SetDirty(_gd);
}
public void QueueWriteToReadBarriers(CommandBufferScoped cbs, PipelineStageFlags stageFlags)
@@ -211,7 +197,7 @@ namespace Ryujinx.Graphics.Vulkan
TextureView dummyTexture,
SamplerHolder dummySampler)
{
- if (_cachedDescriptorSets != null)
+ if (TryGetCachedDescriptorSets(cbs, program, setIndex, out DescriptorSet[] sets))
{
// We still need to ensure the current command buffer holds a reference to all used textures.
@@ -224,12 +210,9 @@ namespace Ryujinx.Graphics.Vulkan
GetBufferViews(cbs);
}
- return _cachedDescriptorSets;
+ return sets;
}
- _cachedDscProgram?.ReleaseManualDescriptorSetCollection(_cachedDscSetIndex, _cachedDscIndex);
- var dsc = program.GetNewManualDescriptorSetCollection(cbs.CommandBufferIndex, setIndex, out _cachedDscIndex).Get(cbs);
-
DescriptorSetTemplate template = program.Templates[setIndex];
DescriptorSetTemplateWriter tu = templateUpdater.Begin(template);
@@ -243,24 +226,9 @@ namespace Ryujinx.Graphics.Vulkan
tu.Push(GetBufferViews(cbs));
}
- var sets = dsc.GetSets();
templateUpdater.Commit(_gd, device, sets[0]);
- _cachedDescriptorSets = sets;
- _cachedDscProgram = program;
- _cachedDscSetIndex = setIndex;
return sets;
}
-
- public void IncrementBindCount()
- {
- _bindCount++;
- }
-
- public void DecrementBindCount()
- {
- int newBindCount = --_bindCount;
- Debug.Assert(newBindCount >= 0);
- }
}
}
From 1828bc949e23c11aba728867376db69bd9e81674 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 15 Jun 2024 22:51:50 +0200
Subject: [PATCH 038/109] nuget: bump Microsoft.IO.RecyclableMemoryStream from
3.0.0 to 3.0.1 (#6936)
Bumps [Microsoft.IO.RecyclableMemoryStream](https://github.com/Microsoft/Microsoft.IO.RecyclableMemoryStream) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/Microsoft/Microsoft.IO.RecyclableMemoryStream/releases)
- [Changelog](https://github.com/microsoft/Microsoft.IO.RecyclableMemoryStream/blob/master/CHANGES.md)
- [Commits](https://github.com/Microsoft/Microsoft.IO.RecyclableMemoryStream/compare/3.0.0...v3.0.1)
---
updated-dependencies:
- dependency-name: Microsoft.IO.RecyclableMemoryStream
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Directory.Packages.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index c6cc01f9a..0514d8aea 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -22,7 +22,7 @@
-
+
From 5a878ae9afe73d12bd344c139ee1b485335af3ff Mon Sep 17 00:00:00 2001
From: jhorv <38920027+jhorv@users.noreply.github.com>
Date: Sat, 15 Jun 2024 17:00:13 -0400
Subject: [PATCH 039/109] replace `ByteMemoryPool` use with `MemoryOwner`
and `SpanOwner` (#6911)
---
.../SDL2HardwareDeviceSession.cs | 4 ++--
.../SoundIoHardwareDeviceSession.cs | 4 ++--
src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs | 6 +++---
3 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs
index 62fe5025d..4eb75a578 100644
--- a/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs
+++ b/src/Ryujinx.Audio.Backends.SDL2/SDL2HardwareDeviceSession.cs
@@ -89,9 +89,9 @@ namespace Ryujinx.Audio.Backends.SDL2
return;
}
- using IMemoryOwner samplesOwner = ByteMemoryPool.Rent(frameCount * _bytesPerFrame);
+ using SpanOwner samplesOwner = SpanOwner.Rent(frameCount * _bytesPerFrame);
- Span samples = samplesOwner.Memory.Span;
+ Span samples = samplesOwner.Span;
_ringBuffer.Read(samples, 0, samples.Length);
diff --git a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs
index 4011a1214..e9cc6a8e1 100644
--- a/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs
+++ b/src/Ryujinx.Audio.Backends.SoundIo/SoundIoHardwareDeviceSession.cs
@@ -122,9 +122,9 @@ namespace Ryujinx.Audio.Backends.SoundIo
int channelCount = areas.Length;
- using IMemoryOwner samplesOwner = ByteMemoryPool.Rent(frameCount * bytesPerFrame);
+ using SpanOwner samplesOwner = SpanOwner.Rent(frameCount * bytesPerFrame);
- Span samples = samplesOwner.Memory.Span;
+ Span samples = samplesOwner.Span;
_ringBuffer.Read(samples, 0, samples.Length);
diff --git a/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs b/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs
index b95e5bed1..7aefe8865 100644
--- a/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs
+++ b/src/Ryujinx.Audio/Backends/Common/DynamicRingBuffer.cs
@@ -14,7 +14,7 @@ namespace Ryujinx.Audio.Backends.Common
private readonly object _lock = new();
- private IMemoryOwner _bufferOwner;
+ private MemoryOwner _bufferOwner;
private Memory _buffer;
private int _size;
private int _headOffset;
@@ -24,7 +24,7 @@ namespace Ryujinx.Audio.Backends.Common
public DynamicRingBuffer(int initialCapacity = RingBufferAlignment)
{
- _bufferOwner = ByteMemoryPool.RentCleared(initialCapacity);
+ _bufferOwner = MemoryOwner.RentCleared(initialCapacity);
_buffer = _bufferOwner.Memory;
}
@@ -62,7 +62,7 @@ namespace Ryujinx.Audio.Backends.Common
private void SetCapacityLocked(int capacity)
{
- IMemoryOwner newBufferOwner = ByteMemoryPool.RentCleared(capacity);
+ MemoryOwner newBufferOwner = MemoryOwner.RentCleared(capacity);
Memory newBuffer = newBufferOwner.Memory;
if (_size > 0)
From 3193ef10833bc0d27e2701c7759ab02674d672d3 Mon Sep 17 00:00:00 2001
From: gdkchan
Date: Sun, 16 Jun 2024 14:46:27 -0300
Subject: [PATCH 040/109] Extend bindless elimination to catch a few more
specific cases (#6921)
* Catch more cases on bindless elimination
* Match blocks with the same comparison condition
* Shader cache version bump
---
.../Shader/DiskCache/DiskCacheHostStorage.cs | 2 +-
.../Instructions/InstEmitPredicate.cs | 2 +-
.../IntermediateRepresentation/Instruction.cs | 20 +++++
.../Optimizations/BindlessElimination.cs | 8 +-
.../Optimizations/BindlessToArray.cs | 8 +-
.../Translation/Optimizations/Optimizer.cs | 21 +----
.../Optimizations/Simplification.cs | 30 ++++++++
.../Translation/Optimizations/Utils.cs | 76 ++++++++++++++++++-
8 files changed, 137 insertions(+), 30 deletions(-)
diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
index fbf48f017..c4b5a1380 100644
--- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
+++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs
@@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
private const ushort FileFormatVersionMajor = 1;
private const ushort FileFormatVersionMinor = 2;
private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
- private const uint CodeGenVersion = 6852;
+ private const uint CodeGenVersion = 6921;
private const string SharedTocFileName = "shared.toc";
private const string SharedDataFileName = "shared.data";
diff --git a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitPredicate.cs b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitPredicate.cs
index 630162ade..1d8651254 100644
--- a/src/Ryujinx.Graphics.Shader/Instructions/InstEmitPredicate.cs
+++ b/src/Ryujinx.Graphics.Shader/Instructions/InstEmitPredicate.cs
@@ -24,7 +24,7 @@ namespace Ryujinx.Graphics.Shader.Instructions
if (op.BVal)
{
- context.Copy(dest, context.ConditionalSelect(res, ConstF(1), Const(0)));
+ context.Copy(dest, context.ConditionalSelect(res, ConstF(1), ConstF(0)));
}
else
{
diff --git a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs
index 8703e660e..273a38a5b 100644
--- a/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs
+++ b/src/Ryujinx.Graphics.Shader/IntermediateRepresentation/Instruction.cs
@@ -156,6 +156,26 @@ namespace Ryujinx.Graphics.Shader.IntermediateRepresentation
return false;
}
+ public static bool IsComparison(this Instruction inst)
+ {
+ switch (inst & Instruction.Mask)
+ {
+ case Instruction.CompareEqual:
+ case Instruction.CompareGreater:
+ case Instruction.CompareGreaterOrEqual:
+ case Instruction.CompareGreaterOrEqualU32:
+ case Instruction.CompareGreaterU32:
+ case Instruction.CompareLess:
+ case Instruction.CompareLessOrEqual:
+ case Instruction.CompareLessOrEqualU32:
+ case Instruction.CompareLessU32:
+ case Instruction.CompareNotEqual:
+ return true;
+ }
+
+ return false;
+ }
+
public static bool IsTextureQuery(this Instruction inst)
{
inst &= Instruction.Mask;
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
index 02a83fbe4..1f2f79a2d 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessElimination.cs
@@ -141,16 +141,16 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
return true;
}
- private static bool IsBindlessAccessAllowed(Operand nvHandle)
+ private static bool IsBindlessAccessAllowed(Operand bindlessHandle)
{
- if (nvHandle.Type == OperandType.ConstantBuffer)
+ if (bindlessHandle.Type == OperandType.ConstantBuffer)
{
// Bindless access with handles from constant buffer is allowed.
return true;
}
- if (nvHandle.AsgOp is not Operation handleOp ||
+ if (bindlessHandle.AsgOp is not Operation handleOp ||
handleOp.Inst != Instruction.Load ||
(handleOp.StorageKind != StorageKind.Input && handleOp.StorageKind != StorageKind.StorageBuffer))
{
@@ -300,7 +300,7 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
resourceManager,
gpuAccessor,
texOp,
- TextureHandle.PackOffsets(src0.GetCbufOffset(), ((src1.Value >> 20) & 0xfff), handleType),
+ TextureHandle.PackOffsets(src0.GetCbufOffset(), (src1.Value >> 20) & 0xfff, handleType),
TextureHandle.PackSlots(src0.GetCbufSlot(), 0),
rewriteSamplerType,
isImage: false);
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToArray.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToArray.cs
index 8eed139d6..1e0b3b645 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToArray.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/BindlessToArray.cs
@@ -126,7 +126,9 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
continue;
}
- if (texOp.GetSource(0).AsgOp is not Operation handleAsgOp)
+ Operand bindlessHandle = Utils.FindLastOperation(texOp.GetSource(0), block);
+
+ if (bindlessHandle.AsgOp is not Operation handleAsgOp)
{
continue;
}
@@ -137,8 +139,8 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
if (handleAsgOp.Inst == Instruction.BitwiseOr)
{
- Operand src0 = handleAsgOp.GetSource(0);
- Operand src1 = handleAsgOp.GetSource(1);
+ Operand src0 = Utils.FindLastOperation(handleAsgOp.GetSource(0), block);
+ Operand src1 = Utils.FindLastOperation(handleAsgOp.GetSource(1), block);
if (src0.Type == OperandType.ConstantBuffer && src1.AsgOp is Operation)
{
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Optimizer.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Optimizer.cs
index 49eb3a89b..1be7c5c52 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Optimizer.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Optimizer.cs
@@ -152,18 +152,14 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
{
// If all phi sources are the same, we can propagate it and remove the phi.
- Operand firstSrc = phi.GetSource(0);
-
- for (int index = 1; index < phi.SourcesCount; index++)
+ if (!Utils.AreAllSourcesTheSameOperand(phi))
{
- if (!IsSameOperand(firstSrc, phi.GetSource(index)))
- {
- return false;
- }
+ return false;
}
// All sources are equal, we can propagate the value.
+ Operand firstSrc = phi.GetSource(0);
Operand dest = phi.Dest;
INode[] uses = dest.UseOps.ToArray();
@@ -182,17 +178,6 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
return true;
}
- private static bool IsSameOperand(Operand x, Operand y)
- {
- if (x.Type != y.Type || x.Value != y.Value)
- {
- return false;
- }
-
- // TODO: Handle Load operations with the same storage and the same constant parameters.
- return x.Type == OperandType.Constant || x.Type == OperandType.ConstantBuffer;
- }
-
private static bool PropagatePack(Operation packOp)
{
// Propagate pack source operands to uses by unpack
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Simplification.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Simplification.cs
index a509fcb42..097c8aa88 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Simplification.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Simplification.cs
@@ -31,6 +31,10 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
TryEliminateBitwiseOr(operation);
break;
+ case Instruction.CompareNotEqual:
+ TryEliminateCompareNotEqual(operation);
+ break;
+
case Instruction.ConditionalSelect:
TryEliminateConditionalSelect(operation);
break;
@@ -174,6 +178,32 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
}
}
+ private static void TryEliminateCompareNotEqual(Operation operation)
+ {
+ // Comparison instruction returns 0 if the result is false, and -1 if true.
+ // Doing a not equal zero comparison on the result is redundant, so we can just copy the first result in this case.
+
+ Operand lhs = operation.GetSource(0);
+ Operand rhs = operation.GetSource(1);
+
+ if (lhs.Type == OperandType.Constant)
+ {
+ (lhs, rhs) = (rhs, lhs);
+ }
+
+ if (rhs.Type != OperandType.Constant || rhs.Value != 0)
+ {
+ return;
+ }
+
+ if (lhs.AsgOp is not Operation compareOp || !compareOp.Inst.IsComparison())
+ {
+ return;
+ }
+
+ operation.TurnIntoCopy(lhs);
+ }
+
private static void TryEliminateConditionalSelect(Operation operation)
{
Operand cond = operation.GetSource(0);
diff --git a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Utils.cs b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Utils.cs
index 74a6d5a1e..23180ff82 100644
--- a/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Utils.cs
+++ b/src/Ryujinx.Graphics.Shader/Translation/Optimizations/Utils.cs
@@ -34,6 +34,50 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
return elemIndexSrc.Type == OperandType.Constant && elemIndexSrc.Value == elemIndex;
}
+ private static bool IsSameOperand(Operand x, Operand y)
+ {
+ if (x.Type != y.Type || x.Value != y.Value)
+ {
+ return false;
+ }
+
+ // TODO: Handle Load operations with the same storage and the same constant parameters.
+ return x == y || x.Type == OperandType.Constant || x.Type == OperandType.ConstantBuffer;
+ }
+
+ private static bool AreAllSourcesEqual(INode node, INode otherNode)
+ {
+ if (node.SourcesCount != otherNode.SourcesCount)
+ {
+ return false;
+ }
+
+ for (int index = 0; index < node.SourcesCount; index++)
+ {
+ if (!IsSameOperand(node.GetSource(index), otherNode.GetSource(index)))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public static bool AreAllSourcesTheSameOperand(INode node)
+ {
+ Operand firstSrc = node.GetSource(0);
+
+ for (int index = 1; index < node.SourcesCount; index++)
+ {
+ if (!IsSameOperand(firstSrc, node.GetSource(index)))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
private static Operation FindBranchSource(BasicBlock block)
{
foreach (BasicBlock sourceBlock in block.Predecessors)
@@ -55,6 +99,19 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
return inst == Instruction.BranchIfFalse || inst == Instruction.BranchIfTrue;
}
+ private static bool IsSameCondition(Operand currentCondition, Operand queryCondition)
+ {
+ if (currentCondition == queryCondition)
+ {
+ return true;
+ }
+
+ return currentCondition.AsgOp is Operation currentOperation &&
+ queryCondition.AsgOp is Operation queryOperation &&
+ currentOperation.Inst == queryOperation.Inst &&
+ AreAllSourcesEqual(currentOperation, queryOperation);
+ }
+
private static bool BlockConditionsMatch(BasicBlock currentBlock, BasicBlock queryBlock)
{
// Check if all the conditions for the query block are satisfied by the current block.
@@ -70,10 +127,10 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
return currentBranch != null && queryBranch != null &&
currentBranch.Inst == queryBranch.Inst &&
- currentCondition == queryCondition;
+ IsSameCondition(currentCondition, queryCondition);
}
- public static Operand FindLastOperation(Operand source, BasicBlock block)
+ public static Operand FindLastOperation(Operand source, BasicBlock block, bool recurse = true)
{
if (source.AsgOp is PhiNode phiNode)
{
@@ -84,10 +141,23 @@ namespace Ryujinx.Graphics.Shader.Translation.Optimizations
for (int i = phiNode.SourcesCount - 1; i >= 0; i--)
{
BasicBlock phiBlock = phiNode.GetBlock(i);
+ Operand phiSource = phiNode.GetSource(i);
if (BlockConditionsMatch(block, phiBlock))
{
- return phiNode.GetSource(i);
+ return phiSource;
+ }
+ else if (recurse && phiSource.AsgOp is PhiNode)
+ {
+ // Phi source is another phi.
+ // Let's check if that phi has a block that matches our condition.
+
+ Operand match = FindLastOperation(phiSource, block, false);
+
+ if (match != phiSource)
+ {
+ return match;
+ }
}
}
}
From 311ca3c3f1719c0effeedfb8cf90d9f2675ef8a5 Mon Sep 17 00:00:00 2001
From: jhorv <38920027+jhorv@users.noreply.github.com>
Date: Sun, 16 Jun 2024 16:47:47 -0400
Subject: [PATCH 041/109] fix: for pooled memory used for reference types,
clear it on return to the pool so that it doesn't prevent GC of the instances
it contained (#6937)
---
src/Ryujinx.Common/Memory/MemoryOwner.cs | 2 +-
src/Ryujinx.Common/Memory/SpanOwner.cs | 2 +-
src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs | 2 +-
src/Ryujinx.HLE/HOS/Kernel/Threading/KSynchronization.cs | 2 +-
4 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/Ryujinx.Common/Memory/MemoryOwner.cs b/src/Ryujinx.Common/Memory/MemoryOwner.cs
index 5e567ab8d..b7fe1db77 100644
--- a/src/Ryujinx.Common/Memory/MemoryOwner.cs
+++ b/src/Ryujinx.Common/Memory/MemoryOwner.cs
@@ -124,7 +124,7 @@ namespace Ryujinx.Common.Memory
if (array is not null)
{
- ArrayPool.Shared.Return(array);
+ ArrayPool.Shared.Return(array, RuntimeHelpers.IsReferenceOrContainsReferences());
}
}
diff --git a/src/Ryujinx.Common/Memory/SpanOwner.cs b/src/Ryujinx.Common/Memory/SpanOwner.cs
index a4b4adf32..acb20bcad 100644
--- a/src/Ryujinx.Common/Memory/SpanOwner.cs
+++ b/src/Ryujinx.Common/Memory/SpanOwner.cs
@@ -108,7 +108,7 @@ namespace Ryujinx.Common.Memory
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Dispose()
{
- ArrayPool.Shared.Return(_array);
+ ArrayPool.Shared.Return(_array, RuntimeHelpers.IsReferenceOrContainsReferences());
}
}
}
diff --git a/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs b/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs
index 6595ecef2..91c6bded2 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs
@@ -616,7 +616,7 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall
}
}
- ArrayPool.Shared.Return(syncObjsArray);
+ ArrayPool.Shared.Return(syncObjsArray, true);
return result;
}
diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KSynchronization.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KSynchronization.cs
index b1af06b0d..21c2730bf 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KSynchronization.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KSynchronization.cs
@@ -104,7 +104,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading
}
}
- ArrayPool>.Shared.Return(syncNodesArray);
+ ArrayPool>.Shared.Return(syncNodesArray, true);
}
_context.CriticalSection.Leave();
From d25a084858438dd1188113efb76548916c2da9de Mon Sep 17 00:00:00 2001
From: gdkchan
Date: Wed, 19 Jun 2024 09:25:47 -0300
Subject: [PATCH 042/109] JIT: Ensure entry block has no predecessors on
RegisterUsage pass (#6951)
---
src/ARMeilleure/Translation/ControlFlowGraph.cs | 11 ++++++++++-
src/ARMeilleure/Translation/RegisterUsage.cs | 13 ++++++++++++-
2 files changed, 22 insertions(+), 2 deletions(-)
diff --git a/src/ARMeilleure/Translation/ControlFlowGraph.cs b/src/ARMeilleure/Translation/ControlFlowGraph.cs
index 3ead49c93..45b092ec5 100644
--- a/src/ARMeilleure/Translation/ControlFlowGraph.cs
+++ b/src/ARMeilleure/Translation/ControlFlowGraph.cs
@@ -11,7 +11,7 @@ namespace ARMeilleure.Translation
private int[] _postOrderMap;
public int LocalsCount { get; private set; }
- public BasicBlock Entry { get; }
+ public BasicBlock Entry { get; private set; }
public IntrusiveList Blocks { get; }
public BasicBlock[] PostOrderBlocks => _postOrderBlocks;
public int[] PostOrderMap => _postOrderMap;
@@ -34,6 +34,15 @@ namespace ARMeilleure.Translation
return result;
}
+ public void UpdateEntry(BasicBlock newEntry)
+ {
+ newEntry.AddSuccessor(Entry);
+
+ Entry = newEntry;
+ Blocks.AddFirst(newEntry);
+ Update();
+ }
+
public void Update()
{
RemoveUnreachableBlocks(Blocks);
diff --git a/src/ARMeilleure/Translation/RegisterUsage.cs b/src/ARMeilleure/Translation/RegisterUsage.cs
index c8c250626..472b0f67b 100644
--- a/src/ARMeilleure/Translation/RegisterUsage.cs
+++ b/src/ARMeilleure/Translation/RegisterUsage.cs
@@ -89,6 +89,17 @@ namespace ARMeilleure.Translation
public static void RunPass(ControlFlowGraph cfg, ExecutionMode mode)
{
+ if (cfg.Entry.Predecessors.Count != 0)
+ {
+ // We expect the entry block to have no predecessors.
+ // This is required because we have a implicit context load at the start of the function,
+ // but if there is a jump to the start of the function, the context load would trash the modified values.
+ // Here we insert a new entry block that will jump to the existing entry block.
+ BasicBlock newEntry = new BasicBlock(cfg.Blocks.Count);
+
+ cfg.UpdateEntry(newEntry);
+ }
+
// Compute local register inputs and outputs used inside blocks.
RegisterMask[] localInputs = new RegisterMask[cfg.Blocks.Count];
RegisterMask[] localOutputs = new RegisterMask[cfg.Blocks.Count];
@@ -201,7 +212,7 @@ namespace ARMeilleure.Translation
// The only block without any predecessor should be the entry block.
// It always needs a context load as it is the first block to run.
- if (block.Predecessors.Count == 0 || hasContextLoad)
+ if (block == cfg.Entry || hasContextLoad)
{
long vecMask = globalInputs[block.Index].VecMask;
long intMask = globalInputs[block.Index].IntMask;
From 0afa8f2c14f046b46ac5ba14c96f3a5ce523ba16 Mon Sep 17 00:00:00 2001
From: gdkchan
Date: Wed, 19 Jun 2024 09:39:29 -0300
Subject: [PATCH 043/109] JIT: Coalesce copies on LSRA with simple register
preferencing (#6950)
* JIT: Coalesce copies on LSRA with simple register preferencing
* PPTC version bump
---
.../RegisterAllocators/LinearScanAllocator.cs | 39 ++++++++++++++++---
.../RegisterAllocators/LiveInterval.cs | 21 ++++++++++
src/ARMeilleure/Translation/PTC/Ptc.cs | 2 +-
3 files changed, 56 insertions(+), 6 deletions(-)
diff --git a/src/ARMeilleure/CodeGen/RegisterAllocators/LinearScanAllocator.cs b/src/ARMeilleure/CodeGen/RegisterAllocators/LinearScanAllocator.cs
index f156e0886..16feeb914 100644
--- a/src/ARMeilleure/CodeGen/RegisterAllocators/LinearScanAllocator.cs
+++ b/src/ARMeilleure/CodeGen/RegisterAllocators/LinearScanAllocator.cs
@@ -251,7 +251,20 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
}
}
- int selectedReg = GetHighestValueIndex(freePositions);
+ // If this is a copy destination variable, we prefer the register used for the copy source.
+ // If the register is available, then the copy can be eliminated later as both source
+ // and destination will use the same register.
+ int selectedReg;
+
+ if (current.TryGetCopySourceRegister(out int preferredReg) && freePositions[preferredReg] >= current.GetEnd())
+ {
+ selectedReg = preferredReg;
+ }
+ else
+ {
+ selectedReg = GetHighestValueIndex(freePositions);
+ }
+
int selectedNextUse = freePositions[selectedReg];
// Intervals starts and ends at odd positions, unless they span an entire
@@ -431,7 +444,7 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
}
}
- private static int GetHighestValueIndex(Span span)
+ private static int GetHighestValueIndex(ReadOnlySpan span)
{
int highest = int.MinValue;
@@ -798,12 +811,12 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
// The "visited" state is stored in the MSB of the local's value.
const ulong VisitedMask = 1ul << 63;
- bool IsVisited(Operand local)
+ static bool IsVisited(Operand local)
{
return (local.GetValueUnsafe() & VisitedMask) != 0;
}
- void SetVisited(Operand local)
+ static void SetVisited(Operand local)
{
local.GetValueUnsafe() |= VisitedMask;
}
@@ -826,9 +839,25 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
{
dest.NumberLocal(_intervals.Count);
- _intervals.Add(new LiveInterval(dest));
+ LiveInterval interval = new LiveInterval(dest);
+ _intervals.Add(interval);
SetVisited(dest);
+
+ // If this is a copy (or copy-like operation), set the copy source interval as well.
+ // This is used for register preferencing later on, which allows the copy to be eliminated
+ // in some cases.
+ if (node.Instruction == Instruction.Copy || node.Instruction == Instruction.ZeroExtend32)
+ {
+ Operand source = node.GetSource(0);
+
+ if (source.Kind == OperandKind.LocalVariable &&
+ source.GetLocalNumber() > 0 &&
+ (node.Instruction == Instruction.Copy || source.Type == OperandType.I32))
+ {
+ interval.SetCopySource(_intervals[source.GetLocalNumber()]);
+ }
+ }
}
}
}
diff --git a/src/ARMeilleure/CodeGen/RegisterAllocators/LiveInterval.cs b/src/ARMeilleure/CodeGen/RegisterAllocators/LiveInterval.cs
index 333d3951b..cfe1bc7ca 100644
--- a/src/ARMeilleure/CodeGen/RegisterAllocators/LiveInterval.cs
+++ b/src/ARMeilleure/CodeGen/RegisterAllocators/LiveInterval.cs
@@ -19,6 +19,7 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
public LiveRange CurrRange;
public LiveInterval Parent;
+ public LiveInterval CopySource;
public UseList Uses;
public LiveIntervalList Children;
@@ -37,6 +38,7 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
private ref LiveRange CurrRange => ref _data->CurrRange;
private ref LiveRange PrevRange => ref _data->PrevRange;
private ref LiveInterval Parent => ref _data->Parent;
+ private ref LiveInterval CopySource => ref _data->CopySource;
private ref UseList Uses => ref _data->Uses;
private ref LiveIntervalList Children => ref _data->Children;
@@ -78,6 +80,25 @@ namespace ARMeilleure.CodeGen.RegisterAllocators
Register = register;
}
+ public void SetCopySource(LiveInterval copySource)
+ {
+ CopySource = copySource;
+ }
+
+ public bool TryGetCopySourceRegister(out int copySourceRegIndex)
+ {
+ if (CopySource._data != null)
+ {
+ copySourceRegIndex = CopySource.Register.Index;
+
+ return true;
+ }
+
+ copySourceRegIndex = 0;
+
+ return false;
+ }
+
public void Reset()
{
PrevRange = default;
diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs
index f56bdce1c..c2eed7a55 100644
--- a/src/ARMeilleure/Translation/PTC/Ptc.cs
+++ b/src/ARMeilleure/Translation/PTC/Ptc.cs
@@ -29,7 +29,7 @@ namespace ARMeilleure.Translation.PTC
private const string OuterHeaderMagicString = "PTCohd\0\0";
private const string InnerHeaderMagicString = "PTCihd\0\0";
- private const uint InternalVersion = 6634; //! To be incremented manually for each change to the ARMeilleure project.
+ private const uint InternalVersion = 6950; //! To be incremented manually for each change to the ARMeilleure project.
private const string ActualDir = "0";
private const string BackupDir = "1";
From 0c3421973c0e79444ac4c2d8bd3d9932a357bbb9 Mon Sep 17 00:00:00 2001
From: Rafa <61294433+rafadotmoe@users.noreply.github.com>
Date: Tue, 25 Jun 2024 08:40:53 +0100
Subject: [PATCH 044/109] SetProcessMemoryPermission address and size are
always 64-bit (#6977)
---
src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs b/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs
index 91c6bded2..8f104b0b7 100644
--- a/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs
+++ b/src/Ryujinx.HLE/HOS/Kernel/SupervisorCall/Syscall.cs
@@ -1546,8 +1546,8 @@ namespace Ryujinx.HLE.HOS.Kernel.SupervisorCall
#pragma warning disable CA1822 // Mark member as static
public Result SetProcessMemoryPermission(
int handle,
- [PointerSized] ulong src,
- [PointerSized] ulong size,
+ ulong src,
+ ulong size,
KMemoryPermission permission)
{
if (!PageAligned(src))
From a94445b23e408707c595ad1833b9bf1fd89e3335 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Wed, 26 Jun 2024 10:45:51 +0200
Subject: [PATCH 045/109] nuget: bump Microsoft.IdentityModel.JsonWebTokens
from 7.6.0 to 7.6.2 (#6965)
Bumps [Microsoft.IdentityModel.JsonWebTokens](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet) from 7.6.0 to 7.6.2.
- [Release notes](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/releases)
- [Changelog](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/7.6.2/CHANGELOG.md)
- [Commits](https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/compare/7.6.0...7.6.2)
---
updated-dependencies:
- dependency-name: Microsoft.IdentityModel.JsonWebTokens
dependency-type: direct:production
update-type: version-update:semver-patch
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ac_K
---
Directory.Packages.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 0514d8aea..6919a2485 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -20,7 +20,7 @@
-
+
From bd3335c143d2420875ff6ea0abd7487deb5a9ddc Mon Sep 17 00:00:00 2001
From: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
Date: Wed, 26 Jun 2024 11:27:23 +0200
Subject: [PATCH 046/109] Make sure the string is long enough before performing
basic trim (#6982)
---
src/Ryujinx.UI.Common/DiscordIntegrationModule.cs | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs
index fb07195d0..6966038b6 100644
--- a/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs
+++ b/src/Ryujinx.UI.Common/DiscordIntegrationModule.cs
@@ -104,8 +104,13 @@ namespace Ryujinx.UI.Common
// Find the length to trim the string to guarantee we have space for the trailing ellipsis.
int trimLimit = byteLimit - Encoding.UTF8.GetByteCount(Ellipsis);
- // Basic trim to best case scenario of 1 byte characters.
- input = input[..trimLimit];
+ // Make sure the string is long enough to perform the basic trim.
+ // Amount of bytes != Length of the string
+ if (input.Length > trimLimit)
+ {
+ // Basic trim to best case scenario of 1 byte characters.
+ input = input[..trimLimit];
+ }
while (Encoding.UTF8.GetByteCount(input) > trimLimit)
{
From 1a0a351a152f837094699e78f51f8970e131bd1a Mon Sep 17 00:00:00 2001
From: sunshineinabox
Date: Wed, 26 Jun 2024 05:21:44 -0700
Subject: [PATCH 047/109] Resolve some Vulkan validation errors (#6915)
* Fix some validation errors
* Whitespace correction
* Resolve some runtime validation errors.
* Whitespace
* Properly fix usage realted validation error by setting Extended Usage image creation flag.
* Only if supported
* Remove checking extension for features that are core functionality of Vulkan 1.2
---
src/Ryujinx.Graphics.Vulkan/BufferManager.cs | 7 +++++++
src/Ryujinx.Graphics.Vulkan/TextureStorage.cs | 2 +-
src/Ryujinx.Graphics.Vulkan/TextureView.cs | 8 ++++----
.../VulkanInitialization.cs | 19 ++++++++++++++++---
4 files changed, 28 insertions(+), 8 deletions(-)
diff --git a/src/Ryujinx.Graphics.Vulkan/BufferManager.cs b/src/Ryujinx.Graphics.Vulkan/BufferManager.cs
index e73cde83c..1b6ac9988 100644
--- a/src/Ryujinx.Graphics.Vulkan/BufferManager.cs
+++ b/src/Ryujinx.Graphics.Vulkan/BufferManager.cs
@@ -103,12 +103,19 @@ namespace Ryujinx.Graphics.Vulkan
usage |= BufferUsageFlags.IndirectBufferBit;
}
+ var externalMemoryBuffer = new ExternalMemoryBufferCreateInfo
+ {
+ SType = StructureType.ExternalMemoryBufferCreateInfo,
+ HandleTypes = ExternalMemoryHandleTypeFlags.HostAllocationBitExt,
+ };
+
var bufferCreateInfo = new BufferCreateInfo
{
SType = StructureType.BufferCreateInfo,
Size = (ulong)size,
Usage = usage,
SharingMode = SharingMode.Exclusive,
+ PNext = &externalMemoryBuffer,
};
gd.Api.CreateBuffer(_device, in bufferCreateInfo, null, out var buffer).ThrowOnError();
diff --git a/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs b/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs
index 230dbd4e8..1aaf2fbbe 100644
--- a/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs
+++ b/src/Ryujinx.Graphics.Vulkan/TextureStorage.cs
@@ -80,7 +80,7 @@ namespace Ryujinx.Graphics.Vulkan
var usage = GetImageUsage(info.Format, info.Target, gd.Capabilities.SupportsShaderStorageImageMultisample);
- var flags = ImageCreateFlags.CreateMutableFormatBit;
+ var flags = ImageCreateFlags.CreateMutableFormatBit | ImageCreateFlags.CreateExtendedUsageBit;
// This flag causes mipmapped texture arrays to break on AMD GCN, so for that copy dependencies are forced for aliasing as cube.
bool isCube = info.Target == Target.Cubemap || info.Target == Target.CubemapArray;
diff --git a/src/Ryujinx.Graphics.Vulkan/TextureView.cs b/src/Ryujinx.Graphics.Vulkan/TextureView.cs
index f2aaf4693..520668028 100644
--- a/src/Ryujinx.Graphics.Vulkan/TextureView.cs
+++ b/src/Ryujinx.Graphics.Vulkan/TextureView.cs
@@ -100,7 +100,7 @@ namespace Ryujinx.Graphics.Vulkan
unsafe Auto CreateImageView(ComponentMapping cm, ImageSubresourceRange sr, ImageViewType viewType, ImageUsageFlags usageFlags)
{
- var usage = new ImageViewUsageCreateInfo
+ var imageViewUsage = new ImageViewUsageCreateInfo
{
SType = StructureType.ImageViewUsageCreateInfo,
Usage = usageFlags,
@@ -114,7 +114,7 @@ namespace Ryujinx.Graphics.Vulkan
Format = format,
Components = cm,
SubresourceRange = sr,
- PNext = &usage,
+ PNext = &imageViewUsage,
};
gd.Api.CreateImageView(device, imageCreateInfo, null, out var imageView).ThrowOnError();
@@ -123,7 +123,7 @@ namespace Ryujinx.Graphics.Vulkan
ImageUsageFlags shaderUsage = ImageUsageFlags.SampledBit;
- if (info.Format.IsImageCompatible())
+ if (info.Format.IsImageCompatible() && (_gd.Capabilities.SupportsShaderStorageImageMultisample || !info.Target.IsMultisample()))
{
shaderUsage |= ImageUsageFlags.StorageBit;
}
@@ -154,7 +154,7 @@ namespace Ryujinx.Graphics.Vulkan
}
else
{
- subresourceRange = new ImageSubresourceRange(aspectFlags, (uint)firstLevel, levels, (uint)firstLayer, (uint)info.Depth);
+ subresourceRange = new ImageSubresourceRange(aspectFlags, (uint)firstLevel, 1, (uint)firstLayer, (uint)info.Depth);
_imageView2dArray = CreateImageView(identityComponentMapping, subresourceRange, ImageViewType.Type2DArray, usage);
}
diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs
index d59ca7e0e..5a9844cb9 100644
--- a/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs
+++ b/src/Ryujinx.Graphics.Vulkan/VulkanInitialization.cs
@@ -42,6 +42,8 @@ namespace Ryujinx.Graphics.Vulkan
"VK_EXT_depth_clip_control",
"VK_KHR_portability_subset", // As per spec, we should enable this if present.
"VK_EXT_4444_formats",
+ "VK_KHR_8bit_storage",
+ "VK_KHR_maintenance2",
};
private static readonly string[] _requiredExtensions = {
@@ -355,6 +357,14 @@ namespace Ryujinx.Graphics.Vulkan
features2.PNext = &supportedFeaturesDepthClipControl;
}
+ PhysicalDeviceVulkan12Features supportedPhysicalDeviceVulkan12Features = new()
+ {
+ SType = StructureType.PhysicalDeviceVulkan12Features,
+ PNext = features2.PNext,
+ };
+
+ features2.PNext = &supportedPhysicalDeviceVulkan12Features;
+
api.GetPhysicalDeviceFeatures2(physicalDevice.PhysicalDevice, &features2);
var supportedFeatures = features2.Features;
@@ -382,6 +392,7 @@ namespace Ryujinx.Graphics.Vulkan
TessellationShader = supportedFeatures.TessellationShader,
VertexPipelineStoresAndAtomics = supportedFeatures.VertexPipelineStoresAndAtomics,
RobustBufferAccess = useRobustBufferAccess,
+ SampleRateShading = supportedFeatures.SampleRateShading,
};
void* pExtendedFeatures = null;
@@ -451,9 +462,11 @@ namespace Ryujinx.Graphics.Vulkan
{
SType = StructureType.PhysicalDeviceVulkan12Features,
PNext = pExtendedFeatures,
- DescriptorIndexing = physicalDevice.IsDeviceExtensionPresent("VK_EXT_descriptor_indexing"),
- DrawIndirectCount = physicalDevice.IsDeviceExtensionPresent(KhrDrawIndirectCount.ExtensionName),
- UniformBufferStandardLayout = physicalDevice.IsDeviceExtensionPresent("VK_KHR_uniform_buffer_standard_layout"),
+ DescriptorIndexing = supportedPhysicalDeviceVulkan12Features.DescriptorIndexing,
+ DrawIndirectCount = supportedPhysicalDeviceVulkan12Features.DrawIndirectCount,
+ UniformBufferStandardLayout = supportedPhysicalDeviceVulkan12Features.UniformBufferStandardLayout,
+ UniformAndStorageBuffer8BitAccess = supportedPhysicalDeviceVulkan12Features.UniformAndStorageBuffer8BitAccess,
+ StorageBuffer8BitAccess = supportedPhysicalDeviceVulkan12Features.StorageBuffer8BitAccess,
};
pExtendedFeatures = &featuresVk12;
From c525d7d9a9a29fcca33ac3e12cef3c6c7e94b158 Mon Sep 17 00:00:00 2001
From: gdkchan
Date: Sun, 7 Jul 2024 19:02:11 -0300
Subject: [PATCH 048/109] Force Vulkan swapchain re-creation when window size
changes (#7003)
---
src/Ryujinx.Graphics.Vulkan/Window.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/Ryujinx.Graphics.Vulkan/Window.cs b/src/Ryujinx.Graphics.Vulkan/Window.cs
index a4ac9e9f1..efb0b31f9 100644
--- a/src/Ryujinx.Graphics.Vulkan/Window.cs
+++ b/src/Ryujinx.Graphics.Vulkan/Window.cs
@@ -623,7 +623,8 @@ namespace Ryujinx.Graphics.Vulkan
public override void SetSize(int width, int height)
{
- // Not needed as we can get the size from the surface.
+ // We don't need to use width and height as we can get the size from the surface.
+ _swapchainIsDirty = true;
}
public override void ChangeVSyncMode(bool vsyncEnabled)
From cfc75d7e78a63fe3bde06b6e4896a42e8dedaf82 Mon Sep 17 00:00:00 2001
From: gdkchan
Date: Sun, 7 Jul 2024 19:19:55 -0300
Subject: [PATCH 049/109] Disable descriptor set template updates for buffer
textures on Adreno (#7002)
* Do not use template updates for buffer textures and buffer images
* No need to do it for images
* Simply buffer texture existence check
* Pipeline is now unused on DescriptorSetUpdater
---
.../DescriptorSetUpdater.cs | 102 ++++++++++++++----
src/Ryujinx.Graphics.Vulkan/PipelineBase.cs | 2 +-
.../ShaderCollection.cs | 16 ++-
3 files changed, 95 insertions(+), 25 deletions(-)
diff --git a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs
index 3590d5d05..75ffca2ca 100644
--- a/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs
+++ b/src/Ryujinx.Graphics.Vulkan/DescriptorSetUpdater.cs
@@ -73,7 +73,6 @@ namespace Ryujinx.Graphics.Vulkan
private readonly VulkanRenderer _gd;
private readonly Device _device;
- private readonly PipelineBase _pipeline;
private ShaderCollection _program;
private readonly BufferRef[] _uniformBufferRefs;
@@ -125,11 +124,10 @@ namespace Ryujinx.Graphics.Vulkan
private readonly TextureView _dummyTexture;
private readonly SamplerHolder _dummySampler;
- public DescriptorSetUpdater(VulkanRenderer gd, Device device, PipelineBase pipeline)
+ public DescriptorSetUpdater(VulkanRenderer gd, Device device)
{
_gd = gd;
_device = device;
- _pipeline = pipeline;
// Some of the bindings counts needs to be multiplied by 2 because we have buffer and
// regular textures/images interleaved on the same descriptor set.
@@ -684,7 +682,14 @@ namespace Ryujinx.Graphics.Vulkan
if (_dirty.HasFlag(DirtyFlags.Texture))
{
- UpdateAndBind(cbs, program, PipelineBase.TextureSetIndex, pbp);
+ if (program.UpdateTexturesWithoutTemplate)
+ {
+ UpdateAndBindTexturesWithoutTemplate(cbs, program, pbp);
+ }
+ else
+ {
+ UpdateAndBind(cbs, program, PipelineBase.TextureSetIndex, pbp);
+ }
}
if (_dirty.HasFlag(DirtyFlags.Image))
@@ -918,31 +923,84 @@ namespace Ryujinx.Graphics.Vulkan
_gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, _program.PipelineLayout, (uint)setIndex, 1, sets, 0, ReadOnlySpan.Empty);
}
- private unsafe void UpdateBuffers(
- CommandBufferScoped cbs,
- PipelineBindPoint pbp,
- int baseBinding,
- ReadOnlySpan bufferInfo,
- DescriptorType type)
+ private void UpdateAndBindTexturesWithoutTemplate(CommandBufferScoped cbs, ShaderCollection program, PipelineBindPoint pbp)
{
- if (bufferInfo.Length == 0)
+ int setIndex = PipelineBase.TextureSetIndex;
+ var bindingSegments = program.BindingSegments[setIndex];
+
+ if (bindingSegments.Length == 0)
{
return;
}
- fixed (DescriptorBufferInfo* pBufferInfo = bufferInfo)
+ if (_updateDescriptorCacheCbIndex)
{
- var writeDescriptorSet = new WriteDescriptorSet
- {
- SType = StructureType.WriteDescriptorSet,
- DstBinding = (uint)baseBinding,
- DescriptorType = type,
- DescriptorCount = (uint)bufferInfo.Length,
- PBufferInfo = pBufferInfo,
- };
-
- _gd.PushDescriptorApi.CmdPushDescriptorSet(cbs.CommandBuffer, pbp, _program.PipelineLayout, 0, 1, &writeDescriptorSet);
+ _updateDescriptorCacheCbIndex = false;
+ program.UpdateDescriptorCacheCommandBufferIndex(cbs.CommandBufferIndex);
}
+
+ var dsc = program.GetNewDescriptorSetCollection(setIndex, out _).Get(cbs);
+
+ foreach (ResourceBindingSegment segment in bindingSegments)
+ {
+ int binding = segment.Binding;
+ int count = segment.Count;
+
+ if (!segment.IsArray)
+ {
+ if (segment.Type != ResourceType.BufferTexture)
+ {
+ Span textures = _textures;
+
+ for (int i = 0; i < count; i++)
+ {
+ ref var texture = ref textures[i];
+ ref var refs = ref _textureRefs[binding + i];
+
+ texture.ImageView = refs.View?.Get(cbs).Value ?? default;
+ texture.Sampler = refs.Sampler?.Get(cbs).Value ?? default;
+
+ if (texture.ImageView.Handle == 0)
+ {
+ texture.ImageView = _dummyTexture.GetImageView().Get(cbs).Value;
+ }
+
+ if (texture.Sampler.Handle == 0)
+ {
+ texture.Sampler = _dummySampler.GetSampler().Get(cbs).Value;
+ }
+ }
+
+ dsc.UpdateImages(0, binding, textures[..count], DescriptorType.CombinedImageSampler);
+ }
+ else
+ {
+ Span bufferTextures = _bufferTextures;
+
+ for (int i = 0; i < count; i++)
+ {
+ bufferTextures[i] = _bufferTextureRefs[binding + i]?.GetBufferView(cbs, false) ?? default;
+ }
+
+ dsc.UpdateBufferImages(0, binding, bufferTextures[..count], DescriptorType.UniformTexelBuffer);
+ }
+ }
+ else
+ {
+ if (segment.Type != ResourceType.BufferTexture)
+ {
+ dsc.UpdateImages(0, binding, _textureArrayRefs[binding].Array.GetImageInfos(_gd, cbs, _dummyTexture, _dummySampler), DescriptorType.CombinedImageSampler);
+ }
+ else
+ {
+ dsc.UpdateBufferImages(0, binding, _textureArrayRefs[binding].Array.GetBufferViews(cbs), DescriptorType.UniformTexelBuffer);
+ }
+ }
+ }
+
+ var sets = dsc.GetSets();
+
+ _gd.Api.CmdBindDescriptorSets(cbs.CommandBuffer, pbp, _program.PipelineLayout, (uint)setIndex, 1, sets, 0, ReadOnlySpan.Empty);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
index 918de59b7..8d7cd5472 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
@@ -105,7 +105,7 @@ namespace Ryujinx.Graphics.Vulkan
gd.Api.CreatePipelineCache(device, pipelineCacheCreateInfo, null, out PipelineCache).ThrowOnError();
- _descriptorSetUpdater = new DescriptorSetUpdater(gd, device, this);
+ _descriptorSetUpdater = new DescriptorSetUpdater(gd, device);
_vertexBufferUpdater = new VertexBufferUpdater(gd);
_transformFeedbackBuffers = new BufferState[Constants.MaxTransformFeedbackBuffers];
diff --git a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs
index f9637789e..eec2a3180 100644
--- a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs
+++ b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs
@@ -23,6 +23,8 @@ namespace Ryujinx.Graphics.Vulkan
public bool IsCompute { get; }
public bool HasTessellationControlShader => (Stages & (1u << 3)) != 0;
+ public bool UpdateTexturesWithoutTemplate { get; }
+
public uint Stages { get; }
public ResourceBindingSegment[][] ClearSegments { get; }
@@ -127,9 +129,12 @@ namespace Ryujinx.Graphics.Vulkan
Stages = stages;
ClearSegments = BuildClearSegments(sets);
- BindingSegments = BuildBindingSegments(resourceLayout.SetUsages);
+ BindingSegments = BuildBindingSegments(resourceLayout.SetUsages, out bool usesBufferTextures);
Templates = BuildTemplates(usePushDescriptors);
+ // Updating buffer texture bindings using template updates crashes the Adreno driver on Windows.
+ UpdateTexturesWithoutTemplate = gd.Vendor == Vendor.Qualcomm && usesBufferTextures;
+
_compileTask = Task.CompletedTask;
_firstBackgroundUse = false;
}
@@ -280,8 +285,10 @@ namespace Ryujinx.Graphics.Vulkan
return segments;
}
- private static ResourceBindingSegment[][] BuildBindingSegments(ReadOnlyCollection setUsages)
+ private static ResourceBindingSegment[][] BuildBindingSegments(ReadOnlyCollection setUsages, out bool usesBufferTextures)
{
+ usesBufferTextures = false;
+
ResourceBindingSegment[][] segments = new ResourceBindingSegment[setUsages.Count][];
for (int setIndex = 0; setIndex < setUsages.Count; setIndex++)
@@ -295,6 +302,11 @@ namespace Ryujinx.Graphics.Vulkan
{
ResourceUsage usage = setUsages[setIndex].Usages[index];
+ if (usage.Type == ResourceType.BufferTexture)
+ {
+ usesBufferTextures = true;
+ }
+
if (currentUsage.Binding + currentCount != usage.Binding ||
currentUsage.Type != usage.Type ||
currentUsage.Stages != usage.Stages ||
From a830eb666b058df38f734c6369eb18cc7212f36c Mon Sep 17 00:00:00 2001
From: gdkchan
Date: Sun, 7 Jul 2024 19:33:28 -0300
Subject: [PATCH 050/109] Disallow concurrent fence waits on Adreno (#7001)
* Disallow concurrent fence waits on Adreno
* Ensure locks are released if exceptions are thrown
---
.../BackgroundResources.cs | 9 ++-
.../CommandBufferPool.cs | 13 ++-
src/Ryujinx.Graphics.Vulkan/FenceHolder.cs | 79 ++++++++++++++++---
.../MultiFenceHolder.cs | 23 +++---
src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs | 6 +-
5 files changed, 108 insertions(+), 22 deletions(-)
diff --git a/src/Ryujinx.Graphics.Vulkan/BackgroundResources.cs b/src/Ryujinx.Graphics.Vulkan/BackgroundResources.cs
index 24e600a26..b8906a62c 100644
--- a/src/Ryujinx.Graphics.Vulkan/BackgroundResources.cs
+++ b/src/Ryujinx.Graphics.Vulkan/BackgroundResources.cs
@@ -29,7 +29,14 @@ namespace Ryujinx.Graphics.Vulkan
lock (queueLock)
{
- _pool = new CommandBufferPool(_gd.Api, _device, queue, queueLock, _gd.QueueFamilyIndex, isLight: true);
+ _pool = new CommandBufferPool(
+ _gd.Api,
+ _device,
+ queue,
+ queueLock,
+ _gd.QueueFamilyIndex,
+ _gd.IsConcurrentFenceWaitUnsupported,
+ isLight: true);
}
}
diff --git a/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs b/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs
index 278dbecfa..80054ce2c 100644
--- a/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs
+++ b/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs
@@ -18,6 +18,7 @@ namespace Ryujinx.Graphics.Vulkan
private readonly Device _device;
private readonly Queue _queue;
private readonly object _queueLock;
+ private readonly bool _concurrentFenceWaitUnsupported;
private readonly CommandPool _pool;
private readonly Thread _owner;
@@ -61,12 +62,20 @@ namespace Ryujinx.Graphics.Vulkan
private int _queuedCount;
private int _inUseCount;
- public unsafe CommandBufferPool(Vk api, Device device, Queue queue, object queueLock, uint queueFamilyIndex, bool isLight = false)
+ public unsafe CommandBufferPool(
+ Vk api,
+ Device device,
+ Queue queue,
+ object queueLock,
+ uint queueFamilyIndex,
+ bool concurrentFenceWaitUnsupported,
+ bool isLight = false)
{
_api = api;
_device = device;
_queue = queue;
_queueLock = queueLock;
+ _concurrentFenceWaitUnsupported = concurrentFenceWaitUnsupported;
_owner = Thread.CurrentThread;
var commandPoolCreateInfo = new CommandPoolCreateInfo
@@ -357,7 +366,7 @@ namespace Ryujinx.Graphics.Vulkan
if (refreshFence)
{
- entry.Fence = new FenceHolder(_api, _device);
+ entry.Fence = new FenceHolder(_api, _device, _concurrentFenceWaitUnsupported);
}
else
{
diff --git a/src/Ryujinx.Graphics.Vulkan/FenceHolder.cs b/src/Ryujinx.Graphics.Vulkan/FenceHolder.cs
index 4f0a87160..0cdb93f20 100644
--- a/src/Ryujinx.Graphics.Vulkan/FenceHolder.cs
+++ b/src/Ryujinx.Graphics.Vulkan/FenceHolder.cs
@@ -10,12 +10,15 @@ namespace Ryujinx.Graphics.Vulkan
private readonly Device _device;
private Fence _fence;
private int _referenceCount;
+ private int _lock;
+ private readonly bool _concurrentWaitUnsupported;
private bool _disposed;
- public unsafe FenceHolder(Vk api, Device device)
+ public unsafe FenceHolder(Vk api, Device device, bool concurrentWaitUnsupported)
{
_api = api;
_device = device;
+ _concurrentWaitUnsupported = concurrentWaitUnsupported;
var fenceCreateInfo = new FenceCreateInfo
{
@@ -47,6 +50,11 @@ namespace Ryujinx.Graphics.Vulkan
}
while (Interlocked.CompareExchange(ref _referenceCount, lastValue + 1, lastValue) != lastValue);
+ if (_concurrentWaitUnsupported)
+ {
+ AcquireLock();
+ }
+
fence = _fence;
return true;
}
@@ -57,6 +65,16 @@ namespace Ryujinx.Graphics.Vulkan
return _fence;
}
+ public void PutLock()
+ {
+ Put();
+
+ if (_concurrentWaitUnsupported)
+ {
+ ReleaseLock();
+ }
+ }
+
public void Put()
{
if (Interlocked.Decrement(ref _referenceCount) == 0)
@@ -66,24 +84,67 @@ namespace Ryujinx.Graphics.Vulkan
}
}
+ private void AcquireLock()
+ {
+ while (!TryAcquireLock())
+ {
+ Thread.SpinWait(32);
+ }
+ }
+
+ private bool TryAcquireLock()
+ {
+ return Interlocked.Exchange(ref _lock, 1) == 0;
+ }
+
+ private void ReleaseLock()
+ {
+ Interlocked.Exchange(ref _lock, 0);
+ }
+
public void Wait()
{
- Span fences = stackalloc Fence[]
+ if (_concurrentWaitUnsupported)
{
- _fence,
- };
+ AcquireLock();
- FenceHelper.WaitAllIndefinitely(_api, _device, fences);
+ try
+ {
+ FenceHelper.WaitAllIndefinitely(_api, _device, stackalloc Fence[] { _fence });
+ }
+ finally
+ {
+ ReleaseLock();
+ }
+ }
+ else
+ {
+ FenceHelper.WaitAllIndefinitely(_api, _device, stackalloc Fence[] { _fence });
+ }
}
public bool IsSignaled()
{
- Span fences = stackalloc Fence[]
+ if (_concurrentWaitUnsupported)
{
- _fence,
- };
+ if (!TryAcquireLock())
+ {
+ return false;
+ }
- return FenceHelper.AllSignaled(_api, _device, fences);
+ try
+ {
+ return FenceHelper.AllSignaled(_api, _device, stackalloc Fence[] { _fence });
+ }
+ finally
+ {
+ ReleaseLock();
+ }
+ }
+ else
+ {
+ return FenceHelper.AllSignaled(_api, _device, stackalloc Fence[] { _fence });
+ }
}
public void Dispose()
diff --git a/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs b/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs
index 806b872bc..b42524712 100644
--- a/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs
+++ b/src/Ryujinx.Graphics.Vulkan/MultiFenceHolder.cs
@@ -196,18 +196,23 @@ namespace Ryujinx.Graphics.Vulkan
bool signaled = true;
- if (hasTimeout)
+ try
{
- signaled = FenceHelper.AllSignaled(api, device, fences[..fenceCount], timeout);
+ if (hasTimeout)
+ {
+ signaled = FenceHelper.AllSignaled(api, device, fences[..fenceCount], timeout);
+ }
+ else
+ {
+ FenceHelper.WaitAllIndefinitely(api, device, fences[..fenceCount]);
+ }
}
- else
+ finally
{
- FenceHelper.WaitAllIndefinitely(api, device, fences[..fenceCount]);
- }
-
- for (int i = 0; i < fenceCount; i++)
- {
- fenceHolders[i].Put();
+ for (int i = 0; i < fenceCount; i++)
+ {
+ fenceHolders[i].PutLock();
+ }
}
return signaled;
diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
index 86a347e01..c16896517 100644
--- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
+++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
@@ -90,6 +90,8 @@ namespace Ryujinx.Graphics.Vulkan
internal bool IsMoltenVk { get; private set; }
internal bool IsTBDR { get; private set; }
internal bool IsSharedMemory { get; private set; }
+ internal bool IsConcurrentFenceWaitUnsupported { get; private set; }
+
public string GpuVendor { get; private set; }
public string GpuDriver { get; private set; }
public string GpuRenderer { get; private set; }
@@ -323,6 +325,8 @@ namespace Ryujinx.Graphics.Vulkan
Vendor == Vendor.Broadcom ||
Vendor == Vendor.ImgTec;
+ IsConcurrentFenceWaitUnsupported = Vendor == Vendor.Qualcomm;
+
GpuVendor = VendorUtils.GetNameFromId(properties.VendorID);
GpuDriver = hasDriverProperties && !OperatingSystem.IsMacOS() ?
VendorUtils.GetFriendlyDriverName(driverProperties.DriverID) : GpuVendor; // Fallback to vendor name if driver is unavailable or on MacOS where vendor is preferred.
@@ -411,7 +415,7 @@ namespace Ryujinx.Graphics.Vulkan
Api.TryGetDeviceExtension(_instance.Instance, _device, out ExtExternalMemoryHost hostMemoryApi);
HostMemoryAllocator = new HostMemoryAllocator(MemoryAllocator, Api, hostMemoryApi, _device);
- CommandBufferPool = new CommandBufferPool(Api, _device, Queue, QueueLock, queueFamilyIndex);
+ CommandBufferPool = new CommandBufferPool(Api, _device, Queue, QueueLock, queueFamilyIndex, IsConcurrentFenceWaitUnsupported);
PipelineLayoutCache = new PipelineLayoutCache();
From 1668ba913fb6019587098ada7431db32d8089951 Mon Sep 17 00:00:00 2001
From: gdkchan
Date: Tue, 9 Jul 2024 23:31:01 -0300
Subject: [PATCH 051/109] Force dynamic state update after rasterizer discard
disable (#7007)
---
src/Ryujinx.Graphics.Vulkan/PipelineBase.cs | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
index 8d7cd5472..00fcc0783 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
@@ -1020,6 +1020,13 @@ namespace Ryujinx.Graphics.Vulkan
{
_newState.RasterizerDiscardEnable = discard;
SignalStateChange();
+
+ if (!discard && Gd.Vendor == Vendor.Qualcomm)
+ {
+ // On Adreno, enabling rasterizer discard somehow corrupts the viewport state.
+ // Force it to be updated on next use to work around this bug.
+ DynamicState.ForceAllDirty();
+ }
}
public void SetRenderTargetColorMasks(ReadOnlySpan componentMask)
From 07435ad844bb615348fa980bb048a89298b6a652 Mon Sep 17 00:00:00 2001
From: gdkchan
Date: Wed, 10 Jul 2024 17:52:45 -0300
Subject: [PATCH 052/109] Use draw clear on Adreno, instead of
vkCmdClearAttachments (#7013)
* Use draw clear on Adreno, instead of vkCmdClearAttachments
* Fix GTX TITAN detection
---
src/Ryujinx.Graphics.Vulkan/BackgroundResources.cs | 2 +-
src/Ryujinx.Graphics.Vulkan/PipelineBase.cs | 2 +-
src/Ryujinx.Graphics.Vulkan/PipelineFull.cs | 6 ++++--
src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs | 2 +-
src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs | 12 ++++++------
5 files changed, 13 insertions(+), 11 deletions(-)
diff --git a/src/Ryujinx.Graphics.Vulkan/BackgroundResources.cs b/src/Ryujinx.Graphics.Vulkan/BackgroundResources.cs
index b8906a62c..0290987fd 100644
--- a/src/Ryujinx.Graphics.Vulkan/BackgroundResources.cs
+++ b/src/Ryujinx.Graphics.Vulkan/BackgroundResources.cs
@@ -35,7 +35,7 @@ namespace Ryujinx.Graphics.Vulkan
queue,
queueLock,
_gd.QueueFamilyIndex,
- _gd.IsConcurrentFenceWaitUnsupported,
+ _gd.IsQualcommProprietary,
isLight: true);
}
}
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
index 00fcc0783..2b2caeaec 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineBase.cs
@@ -1021,7 +1021,7 @@ namespace Ryujinx.Graphics.Vulkan
_newState.RasterizerDiscardEnable = discard;
SignalStateChange();
- if (!discard && Gd.Vendor == Vendor.Qualcomm)
+ if (!discard && Gd.IsQualcommProprietary)
{
// On Adreno, enabling rasterizer discard somehow corrupts the viewport state.
// Force it to be updated on next use to work around this bug.
diff --git a/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs b/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs
index 357d517eb..5808406dc 100644
--- a/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs
+++ b/src/Ryujinx.Graphics.Vulkan/PipelineFull.cs
@@ -47,10 +47,11 @@ namespace Ryujinx.Graphics.Vulkan
return;
}
- if (componentMask != 0xf)
+ if (componentMask != 0xf || Gd.IsQualcommProprietary)
{
// We can't use CmdClearAttachments if not writing all components,
// because on Vulkan, the pipeline state does not affect clears.
+ // On proprietary Adreno drivers, CmdClearAttachments appears to execute out of order, so it's better to not use it at all.
var dstTexture = FramebufferParams.GetColorView(index);
if (dstTexture == null)
{
@@ -87,10 +88,11 @@ namespace Ryujinx.Graphics.Vulkan
return;
}
- if (stencilMask != 0 && stencilMask != 0xff)
+ if ((stencilMask != 0 && stencilMask != 0xff) || Gd.IsQualcommProprietary)
{
// We can't use CmdClearAttachments if not clearing all (mask is all ones, 0xFF) or none (mask is 0) of the stencil bits,
// because on Vulkan, the pipeline state does not affect clears.
+ // On proprietary Adreno drivers, CmdClearAttachments appears to execute out of order, so it's better to not use it at all.
var dstTexture = FramebufferParams.GetDepthStencilView();
if (dstTexture == null)
{
diff --git a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs
index eec2a3180..b1547b795 100644
--- a/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs
+++ b/src/Ryujinx.Graphics.Vulkan/ShaderCollection.cs
@@ -133,7 +133,7 @@ namespace Ryujinx.Graphics.Vulkan
Templates = BuildTemplates(usePushDescriptors);
// Updating buffer texture bindings using template updates crashes the Adreno driver on Windows.
- UpdateTexturesWithoutTemplate = gd.Vendor == Vendor.Qualcomm && usesBufferTextures;
+ UpdateTexturesWithoutTemplate = gd.IsQualcommProprietary && usesBufferTextures;
_compileTask = Task.CompletedTask;
_firstBackgroundUse = false;
diff --git a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
index c16896517..e46eac95f 100644
--- a/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
+++ b/src/Ryujinx.Graphics.Vulkan/VulkanRenderer.cs
@@ -87,10 +87,10 @@ namespace Ryujinx.Graphics.Vulkan
internal bool IsAmdGcn { get; private set; }
internal bool IsNvidiaPreTuring { get; private set; }
internal bool IsIntelArc { get; private set; }
+ internal bool IsQualcommProprietary { get; private set; }
internal bool IsMoltenVk { get; private set; }
internal bool IsTBDR { get; private set; }
internal bool IsSharedMemory { get; private set; }
- internal bool IsConcurrentFenceWaitUnsupported { get; private set; }
public string GpuVendor { get; private set; }
public string GpuDriver { get; private set; }
@@ -325,8 +325,6 @@ namespace Ryujinx.Graphics.Vulkan
Vendor == Vendor.Broadcom ||
Vendor == Vendor.ImgTec;
- IsConcurrentFenceWaitUnsupported = Vendor == Vendor.Qualcomm;
-
GpuVendor = VendorUtils.GetNameFromId(properties.VendorID);
GpuDriver = hasDriverProperties && !OperatingSystem.IsMacOS() ?
VendorUtils.GetFriendlyDriverName(driverProperties.DriverID) : GpuVendor; // Fallback to vendor name if driver is unavailable or on MacOS where vendor is preferred.
@@ -348,7 +346,7 @@ namespace Ryujinx.Graphics.Vulkan
{
IsNvidiaPreTuring = gpuNumber < 2000;
}
- else if (GpuDriver.Contains("TITAN") && !GpuDriver.Contains("RTX"))
+ else if (GpuRenderer.Contains("TITAN") && !GpuRenderer.Contains("RTX"))
{
IsNvidiaPreTuring = true;
}
@@ -358,6 +356,8 @@ namespace Ryujinx.Graphics.Vulkan
IsIntelArc = GpuRenderer.StartsWith("Intel(R) Arc(TM)");
}
+ IsQualcommProprietary = hasDriverProperties && driverProperties.DriverID == DriverId.QualcommProprietary;
+
ulong minResourceAlignment = Math.Max(
Math.Max(
properties.Limits.MinStorageBufferOffsetAlignment,
@@ -415,7 +415,7 @@ namespace Ryujinx.Graphics.Vulkan
Api.TryGetDeviceExtension(_instance.Instance, _device, out ExtExternalMemoryHost hostMemoryApi);
HostMemoryAllocator = new HostMemoryAllocator(MemoryAllocator, Api, hostMemoryApi, _device);
- CommandBufferPool = new CommandBufferPool(Api, _device, Queue, QueueLock, queueFamilyIndex, IsConcurrentFenceWaitUnsupported);
+ CommandBufferPool = new CommandBufferPool(Api, _device, Queue, QueueLock, queueFamilyIndex, IsQualcommProprietary);
PipelineLayoutCache = new PipelineLayoutCache();
@@ -692,7 +692,7 @@ namespace Ryujinx.Graphics.Vulkan
GpuVendor,
memoryType: memoryType,
hasFrontFacingBug: IsIntelWindows,
- hasVectorIndexingBug: Vendor == Vendor.Qualcomm,
+ hasVectorIndexingBug: IsQualcommProprietary,
needsFragmentOutputSpecialization: IsMoltenVk,
reduceShaderPrecision: IsMoltenVk,
supportsAstcCompression: features2.Features.TextureCompressionAstcLdr && supportsAstcFormats,
From 595e514f1804ee2da31ff1b24c2facfc120386d1 Mon Sep 17 00:00:00 2001
From: sunshineinabox
Date: Sun, 14 Jul 2024 01:16:14 -0700
Subject: [PATCH 053/109] Use SkiaSharp for Avalonia in place of ImageSharp
(#6269)
* Rebased
Transformation all at once
Use SkiaSharp instead of ImageSharp
* Apply suggestions from code review
Co-authored-by: Ac_K
* Change back unintentionally changed comment
---------
Co-authored-by: Ac_K
Co-authored-by: Emmanuel Hansen
---
src/Ryujinx/AppHost.cs | 53 ++++++++++++-------
src/Ryujinx/Ryujinx.csproj | 1 -
.../UI/ViewModels/MainWindowViewModel.cs | 15 +++---
.../UserFirmwareAvatarSelectorViewModel.cs | 11 ++--
.../UserFirmwareAvatarSelectorView.axaml.cs | 32 ++++++-----
.../UserProfileImageSelectorView.axaml.cs | 18 ++++---
src/Ryujinx/UI/Windows/IconColorPicker.cs | 45 +++++++++-------
7 files changed, 104 insertions(+), 71 deletions(-)
diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs
index d405f3205..7004908a7 100644
--- a/src/Ryujinx/AppHost.cs
+++ b/src/Ryujinx/AppHost.cs
@@ -40,20 +40,17 @@ using Ryujinx.UI.Common;
using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper;
using Silk.NET.Vulkan;
-using SixLabors.ImageSharp;
-using SixLabors.ImageSharp.Formats.Png;
-using SixLabors.ImageSharp.PixelFormats;
-using SixLabors.ImageSharp.Processing;
+using SkiaSharp;
using SPB.Graphics.Vulkan;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
+using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing;
-using Image = SixLabors.ImageSharp.Image;
using InputManager = Ryujinx.Input.HLE.InputManager;
using IRenderer = Ryujinx.Graphics.GAL.IRenderer;
using Key = Ryujinx.Input.Key;
@@ -366,25 +363,33 @@ namespace Ryujinx.Ava
return;
}
- Image image = e.IsBgra ? Image.LoadPixelData(e.Data, e.Width, e.Height)
- : Image.LoadPixelData(e.Data, e.Width, e.Height);
+ var colorType = e.IsBgra ? SKColorType.Bgra8888 : SKColorType.Rgba8888;
+ using var bitmap = new SKBitmap(new SKImageInfo(e.Width, e.Height, colorType, SKAlphaType.Premul));
- if (e.FlipX)
+ Marshal.Copy(e.Data, 0, bitmap.GetPixels(), e.Data.Length);
+
+ SKBitmap bitmapToSave = null;
+
+ if (e.FlipX || e.FlipY)
{
- image.Mutate(x => x.Flip(FlipMode.Horizontal));
+ bitmapToSave = new SKBitmap(bitmap.Width, bitmap.Height);
+
+ using var canvas = new SKCanvas(bitmapToSave);
+
+ canvas.Clear(SKColors.Transparent);
+
+ float scaleX = e.FlipX ? -1 : 1;
+ float scaleY = e.FlipY ? -1 : 1;
+
+ var matrix = SKMatrix.CreateScale(scaleX, scaleY, bitmap.Width / 2f, bitmap.Height / 2f);
+
+ canvas.SetMatrix(matrix);
+
+ canvas.DrawBitmap(bitmap, new SKPoint(e.FlipX ? -bitmap.Width : 0, e.FlipY ? -bitmap.Height : 0));
}
- if (e.FlipY)
- {
- image.Mutate(x => x.Flip(FlipMode.Vertical));
- }
-
- image.SaveAsPng(path, new PngEncoder
- {
- ColorType = PngColorType.Rgb,
- });
-
- image.Dispose();
+ SaveBitmapAsPng(bitmapToSave ?? bitmap, path);
+ bitmapToSave?.Dispose();
Logger.Notice.Print(LogClass.Application, $"Screenshot saved to {path}", "Screenshot");
}
@@ -396,6 +401,14 @@ namespace Ryujinx.Ava
}
}
+ private void SaveBitmapAsPng(SKBitmap bitmap, string path)
+ {
+ using var data = bitmap.Encode(SKEncodedImageFormat.Png, 100);
+ using var stream = File.OpenWrite(path);
+
+ data.SaveTo(stream);
+ }
+
public void Start()
{
if (OperatingSystem.IsWindows())
diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj
index a43f50063..6718b7fcc 100644
--- a/src/Ryujinx/Ryujinx.csproj
+++ b/src/Ryujinx/Ryujinx.csproj
@@ -54,7 +54,6 @@
-
diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs
index 549eebf14..b47cc4b7c 100644
--- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs
@@ -32,7 +32,7 @@ using Ryujinx.UI.App.Common;
using Ryujinx.UI.Common;
using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper;
-using SixLabors.ImageSharp.PixelFormats;
+using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -40,7 +40,6 @@ using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
-using Image = SixLabors.ImageSharp.Image;
using Key = Ryujinx.Input.Key;
using MissingKeyException = LibHac.Common.Keys.MissingKeyException;
using ShaderCacheLoadingState = Ryujinx.Graphics.Gpu.Shader.ShaderCacheState;
@@ -1164,17 +1163,17 @@ namespace Ryujinx.Ava.UI.ViewModels
private void PrepareLoadScreen()
{
using MemoryStream stream = new(SelectedIcon);
- using var gameIconBmp = Image.Load(stream);
+ using var gameIconBmp = SKBitmap.Decode(stream);
- var dominantColor = IconColorPicker.GetFilteredColor(gameIconBmp).ToPixel();
+ var dominantColor = IconColorPicker.GetFilteredColor(gameIconBmp);
const float ColorMultiple = 0.5f;
- Color progressFgColor = Color.FromRgb(dominantColor.R, dominantColor.G, dominantColor.B);
+ Color progressFgColor = Color.FromRgb(dominantColor.Red, dominantColor.Green, dominantColor.Blue);
Color progressBgColor = Color.FromRgb(
- (byte)(dominantColor.R * ColorMultiple),
- (byte)(dominantColor.G * ColorMultiple),
- (byte)(dominantColor.B * ColorMultiple));
+ (byte)(dominantColor.Red * ColorMultiple),
+ (byte)(dominantColor.Green * ColorMultiple),
+ (byte)(dominantColor.Blue * ColorMultiple));
ProgressBarForegroundColor = new SolidColorBrush(progressFgColor);
ProgressBarBackgroundColor = new SolidColorBrush(progressBgColor);
diff --git a/src/Ryujinx/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs b/src/Ryujinx/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs
index 12adfe94b..b07bf78b9 100644
--- a/src/Ryujinx/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/UserFirmwareAvatarSelectorViewModel.cs
@@ -9,14 +9,14 @@ using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.UI.Models;
using Ryujinx.HLE.FileSystem;
-using SixLabors.ImageSharp;
-using SixLabors.ImageSharp.PixelFormats;
+using SkiaSharp;
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using Color = Avalonia.Media.Color;
+using Image = SkiaSharp.SKImage;
namespace Ryujinx.Ava.UI.ViewModels
{
@@ -130,9 +130,12 @@ namespace Ryujinx.Ava.UI.ViewModels
stream.Position = 0;
- Image avatarImage = Image.LoadPixelData(DecompressYaz0(stream), 256, 256);
+ Image avatarImage = Image.FromPixelCopy(new SKImageInfo(256, 256, SKColorType.Rgba8888, SKAlphaType.Premul), DecompressYaz0(stream));
- avatarImage.SaveAsPng(streamPng);
+ using (SKData data = avatarImage.Encode(SKEncodedImageFormat.Png, 100))
+ {
+ data.SaveTo(streamPng);
+ }
_avatarStore.Add(item.FullPath, streamPng.ToArray());
}
diff --git a/src/Ryujinx/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs b/src/Ryujinx/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs
index b6376866d..064b5e908 100644
--- a/src/Ryujinx/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs
+++ b/src/Ryujinx/UI/Views/User/UserFirmwareAvatarSelectorView.axaml.cs
@@ -6,12 +6,8 @@ using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.HLE.FileSystem;
-using SixLabors.ImageSharp;
-using SixLabors.ImageSharp.Formats.Png;
-using SixLabors.ImageSharp.PixelFormats;
-using SixLabors.ImageSharp.Processing;
+using SkiaSharp;
using System.IO;
-using Image = SixLabors.ImageSharp.Image;
namespace Ryujinx.Ava.UI.Views.User
{
@@ -70,15 +66,25 @@ namespace Ryujinx.Ava.UI.Views.User
{
if (ViewModel.SelectedImage != null)
{
- MemoryStream streamJpg = new();
- Image avatarImage = Image.Load(ViewModel.SelectedImage, new PngDecoder());
+ using var streamJpg = new MemoryStream();
+ using var bitmap = SKBitmap.Decode(ViewModel.SelectedImage);
+ using var newBitmap = new SKBitmap(bitmap.Width, bitmap.Height);
- avatarImage.Mutate(x => x.BackgroundColor(new Rgba32(
- ViewModel.BackgroundColor.R,
- ViewModel.BackgroundColor.G,
- ViewModel.BackgroundColor.B,
- ViewModel.BackgroundColor.A)));
- avatarImage.SaveAsJpeg(streamJpg);
+ using (var canvas = new SKCanvas(newBitmap))
+ {
+ canvas.Clear(new SKColor(
+ ViewModel.BackgroundColor.R,
+ ViewModel.BackgroundColor.G,
+ ViewModel.BackgroundColor.B,
+ ViewModel.BackgroundColor.A));
+ canvas.DrawBitmap(bitmap, 0, 0);
+ }
+
+ using (var image = SKImage.FromBitmap(newBitmap))
+ using (var dataJpeg = image.Encode(SKEncodedImageFormat.Jpeg, 100))
+ {
+ dataJpeg.SaveTo(streamJpg);
+ }
_profile.Image = streamJpg.ToArray();
diff --git a/src/Ryujinx/UI/Views/User/UserProfileImageSelectorView.axaml.cs b/src/Ryujinx/UI/Views/User/UserProfileImageSelectorView.axaml.cs
index fabfaa4e8..b4f23b5b8 100644
--- a/src/Ryujinx/UI/Views/User/UserProfileImageSelectorView.axaml.cs
+++ b/src/Ryujinx/UI/Views/User/UserProfileImageSelectorView.axaml.cs
@@ -9,11 +9,9 @@ using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.HLE.FileSystem;
-using SixLabors.ImageSharp;
-using SixLabors.ImageSharp.Processing;
+using SkiaSharp;
using System.Collections.Generic;
using System.IO;
-using Image = SixLabors.ImageSharp.Image;
namespace Ryujinx.Ava.UI.Views.User
{
@@ -102,13 +100,19 @@ namespace Ryujinx.Ava.UI.Views.User
private static byte[] ProcessProfileImage(byte[] buffer)
{
- using Image image = Image.Load(buffer);
+ using var bitmap = SKBitmap.Decode(buffer);
- image.Mutate(x => x.Resize(256, 256));
+ var resizedBitmap = bitmap.Resize(new SKImageInfo(256, 256), SKFilterQuality.High);
- using MemoryStream streamJpg = new();
+ using var streamJpg = new MemoryStream();
- image.SaveAsJpeg(streamJpg);
+ if (resizedBitmap != null)
+ {
+ using var image = SKImage.FromBitmap(resizedBitmap);
+ using var dataJpeg = image.Encode(SKEncodedImageFormat.Jpeg, 100);
+
+ dataJpeg.SaveTo(streamJpg);
+ }
return streamJpg.ToArray();
}
diff --git a/src/Ryujinx/UI/Windows/IconColorPicker.cs b/src/Ryujinx/UI/Windows/IconColorPicker.cs
index 72660351a..dd6a55d4d 100644
--- a/src/Ryujinx/UI/Windows/IconColorPicker.cs
+++ b/src/Ryujinx/UI/Windows/IconColorPicker.cs
@@ -1,5 +1,4 @@
-using SixLabors.ImageSharp;
-using SixLabors.ImageSharp.PixelFormats;
+using SkiaSharp;
using System;
using System.Collections.Generic;
@@ -36,35 +35,34 @@ namespace Ryujinx.Ava.UI.Windows
}
}
- public static Color GetFilteredColor(Image image)
+ public static SKColor GetFilteredColor(SKBitmap image)
{
- var color = GetColor(image).ToPixel();
+ var color = GetColor(image);
+
// We don't want colors that are too dark.
// If the color is too dark, make it brighter by reducing the range
// and adding a constant color.
- int luminosity = GetColorApproximateLuminosity(color.R, color.G, color.B);
+ int luminosity = GetColorApproximateLuminosity(color.Red, color.Green, color.Blue);
if (luminosity < CutOffLuminosity)
{
- color = Color.FromRgb(
- (byte)Math.Min(CutOffLuminosity + color.R, byte.MaxValue),
- (byte)Math.Min(CutOffLuminosity + color.G, byte.MaxValue),
- (byte)Math.Min(CutOffLuminosity + color.B, byte.MaxValue));
+ color = new SKColor(
+ (byte)Math.Min(CutOffLuminosity + color.Red, byte.MaxValue),
+ (byte)Math.Min(CutOffLuminosity + color.Green, byte.MaxValue),
+ (byte)Math.Min(CutOffLuminosity + color.Blue, byte.MaxValue));
}
return color;
}
- public static Color GetColor(Image image)
+ public static SKColor GetColor(SKBitmap image)
{
var colors = new PaletteColor[TotalColors];
-
var dominantColorBin = new Dictionary();
var buffer = GetBuffer(image);
int w = image.Width;
-
int w8 = w << 8;
int h8 = image.Height << 8;
@@ -84,9 +82,10 @@ namespace Ryujinx.Ava.UI.Windows
{
int offset = x + yOffset;
- byte cb = buffer[offset].B;
- byte cg = buffer[offset].G;
- byte cr = buffer[offset].R;
+ SKColor pixel = buffer[offset];
+ byte cr = pixel.Red;
+ byte cg = pixel.Green;
+ byte cb = pixel.Blue;
var qck = GetQuantizedColorKey(cr, cg, cb);
@@ -122,12 +121,22 @@ namespace Ryujinx.Ava.UI.Windows
}
}
- return Color.FromRgb(bestCandidate.R, bestCandidate.G, bestCandidate.B);
+ return new SKColor(bestCandidate.R, bestCandidate.G, bestCandidate.B);
}
- public static Bgra32[] GetBuffer(Image image)
+ public static SKColor[] GetBuffer(SKBitmap image)
{
- return image.DangerousTryGetSinglePixelMemory(out var data) ? data.ToArray() : Array.Empty();
+ var pixels = new SKColor[image.Width * image.Height];
+
+ for (int y = 0; y < image.Height; y++)
+ {
+ for (int x = 0; x < image.Width; x++)
+ {
+ pixels[x + y * image.Width] = image.GetPixel(x, y);
+ }
+ }
+
+ return pixels;
}
private static int GetColorScore(Dictionary dominantColorBin, int maxHitCount, PaletteColor color)
From a6dbb2ad2b7e1e0ed704dc33066f832e2cdd6a4a Mon Sep 17 00:00:00 2001
From: jhorv <38920027+jhorv@users.noreply.github.com>
Date: Mon, 15 Jul 2024 18:21:53 -0400
Subject: [PATCH 054/109] replace ByteMemoryPool usage in Ryujinx.HLE (#6953)
---
src/Ryujinx.HLE/HOS/Services/ServerBase.cs | 4 ++--
.../HOS/Services/SurfaceFlinger/IHOSBinderDriver.cs | 4 ++--
src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/Parcel.cs | 7 +++----
3 files changed, 7 insertions(+), 8 deletions(-)
diff --git a/src/Ryujinx.HLE/HOS/Services/ServerBase.cs b/src/Ryujinx.HLE/HOS/Services/ServerBase.cs
index 5e18d7981..f67699b90 100644
--- a/src/Ryujinx.HLE/HOS/Services/ServerBase.cs
+++ b/src/Ryujinx.HLE/HOS/Services/ServerBase.cs
@@ -474,9 +474,9 @@ namespace Ryujinx.HLE.HOS.Services
{
const int MessageSize = 0x100;
- using IMemoryOwner reqDataOwner = ByteMemoryPool.Rent(MessageSize);
+ using SpanOwner reqDataOwner = SpanOwner.Rent(MessageSize);
- Span reqDataSpan = reqDataOwner.Memory.Span;
+ Span reqDataSpan = reqDataOwner.Span;
_selfProcess.CpuMemory.Read(_selfThread.TlsAddress, reqDataSpan);
diff --git a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/IHOSBinderDriver.cs b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/IHOSBinderDriver.cs
index 7cb6763b8..2ffa961cb 100644
--- a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/IHOSBinderDriver.cs
+++ b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/IHOSBinderDriver.cs
@@ -85,9 +85,9 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
ReadOnlySpan inputParcel = context.Memory.GetSpan(dataPos, (int)dataSize);
- using IMemoryOwner outputParcelOwner = ByteMemoryPool.RentCleared(replySize);
+ using SpanOwner outputParcelOwner = SpanOwner.RentCleared(checked((int)replySize));
- Span outputParcel = outputParcelOwner.Memory.Span;
+ Span outputParcel = outputParcelOwner.Span;
ResultCode result = OnTransact(binderId, code, flags, inputParcel, outputParcel);
diff --git a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/Parcel.cs b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/Parcel.cs
index c6cd60d04..2ca0e1aac 100644
--- a/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/Parcel.cs
+++ b/src/Ryujinx.HLE/HOS/Services/SurfaceFlinger/Parcel.cs
@@ -3,7 +3,6 @@ using Ryujinx.Common.Memory;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.Services.SurfaceFlinger.Types;
using System;
-using System.Buffers;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -13,7 +12,7 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
{
sealed class Parcel : IDisposable
{
- private readonly IMemoryOwner _rawDataOwner;
+ private readonly MemoryOwner _rawDataOwner;
private Span Raw => _rawDataOwner.Memory.Span;
@@ -30,7 +29,7 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
public Parcel(ReadOnlySpan data)
{
- _rawDataOwner = ByteMemoryPool.RentCopy(data);
+ _rawDataOwner = MemoryOwner.RentCopy(data);
_payloadPosition = 0;
_objectPosition = 0;
@@ -40,7 +39,7 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
{
uint headerSize = (uint)Unsafe.SizeOf();
- _rawDataOwner = ByteMemoryPool.RentCleared(BitUtils.AlignUp(headerSize + payloadSize + objectsSize, 4));
+ _rawDataOwner = MemoryOwner.RentCleared(checked((int)BitUtils.AlignUp(headerSize + payloadSize + objectsSize, 4)));
Header.PayloadSize = payloadSize;
Header.ObjectsSize = objectsSize;
From eb212aa91b45945d7bfd1ab19571b095edff5cc7 Mon Sep 17 00:00:00 2001
From: MutantAura <44103205+MutantAura@users.noreply.github.com>
Date: Mon, 15 Jul 2024 23:27:59 +0100
Subject: [PATCH 055/109] misc: Re-order and manually update DriverID to name.
(#7027)
* Re-order and update DriverID -> Name.
* Fix whitespace
---
src/Ryujinx.Graphics.Vulkan/Vendor.cs | 39 +++++++++++++++------------
1 file changed, 22 insertions(+), 17 deletions(-)
diff --git a/src/Ryujinx.Graphics.Vulkan/Vendor.cs b/src/Ryujinx.Graphics.Vulkan/Vendor.cs
index e0f569079..802771ede 100644
--- a/src/Ryujinx.Graphics.Vulkan/Vendor.cs
+++ b/src/Ryujinx.Graphics.Vulkan/Vendor.cs
@@ -69,27 +69,32 @@ namespace Ryujinx.Graphics.Vulkan
{
DriverId.AmdProprietary => "AMD",
DriverId.AmdOpenSource => "AMD (Open)",
- DriverId.ArmProprietary => "ARM",
- DriverId.BroadcomProprietary => "Broadcom",
- DriverId.CoreaviProprietary => "CoreAVI",
- DriverId.GgpProprietary => "GGP",
- DriverId.GoogleSwiftshader => "SwiftShader",
- DriverId.ImaginationProprietary => "Imagination",
- DriverId.IntelOpenSourceMesa => "Intel (Open)",
- DriverId.IntelProprietaryWindows => "Intel",
- DriverId.JuiceProprietary => "Juice",
- DriverId.MesaDozen => "Dozen",
- DriverId.MesaLlvmpipe => "LLVMpipe",
- DriverId.MesaPanvk => "PanVK",
DriverId.MesaRadv => "RADV",
+ DriverId.NvidiaProprietary => "NVIDIA",
+ DriverId.IntelProprietaryWindows => "Intel",
+ DriverId.IntelOpenSourceMesa => "Intel (Open)",
+ DriverId.ImaginationProprietary => "Imagination",
+ DriverId.QualcommProprietary => "Qualcomm",
+ DriverId.ArmProprietary => "ARM",
+ DriverId.GoogleSwiftshader => "SwiftShader",
+ DriverId.GgpProprietary => "GGP",
+ DriverId.BroadcomProprietary => "Broadcom",
+ DriverId.MesaLlvmpipe => "LLVMpipe",
+ DriverId.Moltenvk => "MoltenVK",
+ DriverId.CoreaviProprietary => "CoreAVI",
+ DriverId.JuiceProprietary => "Juice",
+ DriverId.VerisiliconProprietary => "Verisilicon",
DriverId.MesaTurnip => "Turnip",
DriverId.MesaV3DV => "V3DV",
- DriverId.MesaVenus => "Venus",
- DriverId.Moltenvk => "MoltenVK",
- DriverId.NvidiaProprietary => "NVIDIA",
- DriverId.QualcommProprietary => "Qualcomm",
+ DriverId.MesaPanvk => "PanVK",
DriverId.SamsungProprietary => "Samsung",
- DriverId.VerisiliconProprietary => "Verisilicon",
+ DriverId.MesaVenus => "Venus",
+ DriverId.MesaDozen => "Dozen",
+
+ // TODO: Use real enum when we have an up to date Silk.NET.
+ (DriverId)24 => "NVK",
+ (DriverId)25 => "Imagination (Open)",
+ (DriverId)26 => "Honeykrisp",
_ => id.ToString(),
};
}
From 344f4f52c1117028d08802aff60fbd4d875717b4 Mon Sep 17 00:00:00 2001
From: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com>
Date: Tue, 16 Jul 2024 21:01:06 +0100
Subject: [PATCH 056/109] Remove CommandBufferScoped Dependencies (#6958)
---
.../CommandBufferPool.cs | 17 ------
.../CommandBufferScoped.cs | 5 --
.../SemaphoreHolder.cs | 60 -------------------
3 files changed, 82 deletions(-)
delete mode 100644 src/Ryujinx.Graphics.Vulkan/SemaphoreHolder.cs
diff --git a/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs b/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs
index 80054ce2c..e3938392f 100644
--- a/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs
+++ b/src/Ryujinx.Graphics.Vulkan/CommandBufferPool.cs
@@ -31,11 +31,9 @@ namespace Ryujinx.Graphics.Vulkan
public int SubmissionCount;
public CommandBuffer CommandBuffer;
public FenceHolder Fence;
- public SemaphoreHolder Semaphore;
public List Dependants;
public List Waitables;
- public HashSet Dependencies;
public void Initialize(Vk api, Device device, CommandPool pool)
{
@@ -51,7 +49,6 @@ namespace Ryujinx.Graphics.Vulkan
Dependants = new List();
Waitables = new List();
- Dependencies = new HashSet();
}
}
@@ -143,14 +140,6 @@ namespace Ryujinx.Graphics.Vulkan
}
}
- public void AddDependency(int cbIndex, CommandBufferScoped dependencyCbs)
- {
- Debug.Assert(_commandBuffers[cbIndex].InUse);
- var semaphoreHolder = _commandBuffers[dependencyCbs.CommandBufferIndex].Semaphore;
- semaphoreHolder.Get();
- _commandBuffers[cbIndex].Dependencies.Add(semaphoreHolder);
- }
-
public void AddWaitable(int cbIndex, MultiFenceHolder waitable)
{
ref var entry = ref _commandBuffers[cbIndex];
@@ -354,14 +343,8 @@ namespace Ryujinx.Graphics.Vulkan
waitable.RemoveBufferUses(cbIndex);
}
- foreach (var dependency in entry.Dependencies)
- {
- dependency.Put();
- }
-
entry.Dependants.Clear();
entry.Waitables.Clear();
- entry.Dependencies.Clear();
entry.Fence?.Dispose();
if (refreshFence)
diff --git a/src/Ryujinx.Graphics.Vulkan/CommandBufferScoped.cs b/src/Ryujinx.Graphics.Vulkan/CommandBufferScoped.cs
index 270cdc6e6..2accd69b2 100644
--- a/src/Ryujinx.Graphics.Vulkan/CommandBufferScoped.cs
+++ b/src/Ryujinx.Graphics.Vulkan/CommandBufferScoped.cs
@@ -26,11 +26,6 @@ namespace Ryujinx.Graphics.Vulkan
_pool.AddWaitable(CommandBufferIndex, waitable);
}
- public void AddDependency(CommandBufferScoped dependencyCbs)
- {
- _pool.AddDependency(CommandBufferIndex, dependencyCbs);
- }
-
public FenceHolder GetFence()
{
return _pool.GetFence(CommandBufferIndex);
diff --git a/src/Ryujinx.Graphics.Vulkan/SemaphoreHolder.cs b/src/Ryujinx.Graphics.Vulkan/SemaphoreHolder.cs
deleted file mode 100644
index 618a7d488..000000000
--- a/src/Ryujinx.Graphics.Vulkan/SemaphoreHolder.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using Silk.NET.Vulkan;
-using System;
-using System.Threading;
-using VkSemaphore = Silk.NET.Vulkan.Semaphore;
-
-namespace Ryujinx.Graphics.Vulkan
-{
- class SemaphoreHolder : IDisposable
- {
- private readonly Vk _api;
- private readonly Device _device;
- private VkSemaphore _semaphore;
- private int _referenceCount;
- private bool _disposed;
-
- public unsafe SemaphoreHolder(Vk api, Device device)
- {
- _api = api;
- _device = device;
-
- var semaphoreCreateInfo = new SemaphoreCreateInfo
- {
- SType = StructureType.SemaphoreCreateInfo,
- };
-
- api.CreateSemaphore(device, in semaphoreCreateInfo, null, out _semaphore).ThrowOnError();
-
- _referenceCount = 1;
- }
-
- public VkSemaphore GetUnsafe()
- {
- return _semaphore;
- }
-
- public VkSemaphore Get()
- {
- Interlocked.Increment(ref _referenceCount);
- return _semaphore;
- }
-
- public unsafe void Put()
- {
- if (Interlocked.Decrement(ref _referenceCount) == 0)
- {
- _api.DestroySemaphore(_device, _semaphore, null);
- _semaphore = default;
- }
- }
-
- public void Dispose()
- {
- if (!_disposed)
- {
- Put();
- _disposed = true;
- }
- }
- }
-}
From 6fbf279faca30ffd2ef33394463b98809ccaf127 Mon Sep 17 00:00:00 2001
From: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
Date: Tue, 16 Jul 2024 23:17:32 +0200
Subject: [PATCH 057/109] Add support for multi game XCIs (second try) (#6515)
* Add default values to ApplicationData directly
* Refactor application loading
It should now be possible to load multi game XCIs.
Included updates won't be detected for now.
Opening a game from the command line currently only opens the first one.
* Only include program NCAs where at least one tuple item is not null
* Get application data by title id and add programIndex check back
* Refactor application loading again and remove duplicate code
* Actually use patch ncas for updates
* Fix number of applications found with multi game xcis
* Don't load bundled updates from multi game xcis
* Change ApplicationData.TitleId type to ulong & Add TitleIdString property
* Use cnmt files and ContentCollection to load programs
* Ava: Add updates and DLCs from gamecarts
* Get the cnmt file from its NCA
* Ava: Identify bundled updates in updater window
* Fix the (hopefully) last few bugs
* Add idOffset parameter to GetNcaByType
* Handle missing file for dlc.json
* Ava: Shorten error message for invalid files
* Gtk: Add additional string for bundled updates in TitleUpdateWindow
* Hopefully fix DLC issues
* Apply formatting
* Finally fix DLC issues
* Adjust property names and fileSize field
* Read the correct update file
* Fix wrong casing for application id strings
* Rename TitleId to ApplicationId
* Address review comments
* Apply suggestions from code review
Co-authored-by: gdkchan
* Gracefully fail when loading pfs for update and dlc window
* Fix applications with multiple programs
* Fix DLCWindow crash on GTK
* Fix some GUI issues
* Remove IsXci again
* Don't add duplicates to update/dlc windows
* Avoid double lookup
* Preserve DLC enabled state for bundled DLCs
* Fix DLCWindow not opening using GTK
* Fix missing information when loading applications from file
* Address review feedback
Rename ContentCollection to ContentMetaData
Fix casing issues in log messages
Use null as the default value for updatePath
* Fix re-adding bundled DLCs every time
* Fix bundled DLCs disappearing
* Abstract common code to open application pfs
* Remove unused imports
* Fix file exists check when loading DLCs
* Load bundled DLCs only using dlc.json
* Load AoC items correctly
* Add all DLCs from a PFS
* Add argument to launch a specific application id
* Use application-id argument for shortcuts if necessary
* Return the application id from the control NCA if possible
* GetApplicationInformation: Don't overwrite application ids
Move SaveDataOwnerId check to the top, since it seems to be more reliable.
* Get application ids from CNMT again
This commit reverts some parts of 61615b8f0d6f90ae86778958ddc38eaf6dc280ab.
Since the issue wasn't actually related to the application id in CMNTs, we can remove the wrong assumptions.
* Revert erroneous axaml change from adca8900
* Rename title to application
* Wrap nsp/pfs0 case with curly braces
* Check if _applicationData.ControlHolder.ByteSpan is zeros only once
* Catch exceptions while loading applications from nsps
---------
Co-authored-by: gdkchan
---
src/Ryujinx.Gtk3/Program.cs | 31 +-
src/Ryujinx.Gtk3/UI/MainWindow.cs | 107 +-
.../UI/Widgets/GameTableContextMenu.cs | 108 +-
src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs | 9 +-
src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs | 128 +--
.../UI/Windows/TitleUpdateWindow.cs | 98 +-
src/Ryujinx.HLE/FileSystem/ContentManager.cs | 42 +-
src/Ryujinx.HLE/FileSystem/ContentMetaData.cs | 61 ++
.../Extensions/LocalFileSystemExtensions.cs | 1 -
.../Processes/Extensions/NcaExtensions.cs | 86 +-
.../PartitionFileSystemExtensions.cs | 133 +--
.../Loaders/Processes/ProcessLoader.cs | 8 +-
.../Loaders/Processes/ProcessLoaderHelper.cs | 9 +-
src/Ryujinx.HLE/Switch.cs | 8 +-
.../Utilities/PartitionFileSystemUtils.cs | 45 +
src/Ryujinx.UI.Common/App/ApplicationData.cs | 18 +-
.../App/ApplicationLibrary.cs | 954 +++++++++---------
.../Helper/CommandLineState.cs | 5 +
.../Helper/ShortcutHelper.cs | 26 +-
src/Ryujinx/AppHost.cs | 7 +-
src/Ryujinx/Assets/Locales/en_US.json | 3 +
src/Ryujinx/Common/ApplicationHelper.cs | 9 +-
src/Ryujinx/Program.cs | 2 +-
.../Controls/ApplicationContextMenu.axaml.cs | 56 +-
.../UI/Controls/ApplicationGridView.axaml | 2 +-
.../UI/Controls/ApplicationListView.axaml | 4 +-
.../UI/Models/DownloadableContentModel.cs | 4 +
src/Ryujinx/UI/Models/SaveModel.cs | 4 +-
src/Ryujinx/UI/Models/TitleUpdateModel.cs | 5 +-
.../DownloadableContentManagerViewModel.cs | 66 +-
.../UI/ViewModels/MainWindowViewModel.cs | 50 +-
.../UI/ViewModels/TitleUpdateViewModel.cs | 77 +-
.../UI/Views/Main/MainMenuBarView.axaml.cs | 10 +-
src/Ryujinx/UI/Windows/CheatWindow.axaml.cs | 7 +-
.../DownloadableContentManagerWindow.axaml | 2 +-
.../DownloadableContentManagerWindow.axaml.cs | 13 +-
src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 47 +-
.../UI/Windows/TitleUpdateWindow.axaml.cs | 15 +-
38 files changed, 1299 insertions(+), 961 deletions(-)
create mode 100644 src/Ryujinx.HLE/FileSystem/ContentMetaData.cs
create mode 100644 src/Ryujinx.HLE/Utilities/PartitionFileSystemUtils.cs
diff --git a/src/Ryujinx.Gtk3/Program.cs b/src/Ryujinx.Gtk3/Program.cs
index 749cb6978..0fb712885 100644
--- a/src/Ryujinx.Gtk3/Program.cs
+++ b/src/Ryujinx.Gtk3/Program.cs
@@ -7,6 +7,7 @@ using Ryujinx.Common.SystemInterop;
using Ryujinx.Modules;
using Ryujinx.SDL2.Common;
using Ryujinx.UI;
+using Ryujinx.UI.App.Common;
using Ryujinx.UI.Common;
using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper;
@@ -322,7 +323,35 @@ namespace Ryujinx
if (CommandLineState.LaunchPathArg != null)
{
- mainWindow.RunApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg);
+ if (mainWindow.ApplicationLibrary.TryGetApplicationsFromFile(CommandLineState.LaunchPathArg, out List applications))
+ {
+ ApplicationData applicationData;
+
+ if (CommandLineState.LaunchApplicationId != null)
+ {
+ applicationData = applications.Find(application => application.IdString == CommandLineState.LaunchApplicationId);
+
+ if (applicationData != null)
+ {
+ mainWindow.RunApplication(applicationData, CommandLineState.StartFullscreenArg);
+ }
+ else
+ {
+ Logger.Error?.Print(LogClass.Application, $"Couldn't find requested application id '{CommandLineState.LaunchApplicationId}' in '{CommandLineState.LaunchPathArg}'.");
+ UserErrorDialog.CreateUserErrorDialog(UserError.ApplicationNotFound);
+ }
+ }
+ else
+ {
+ applicationData = applications[0];
+ mainWindow.RunApplication(applicationData, CommandLineState.StartFullscreenArg);
+ }
+ }
+ else
+ {
+ Logger.Error?.Print(LogClass.Application, $"Couldn't find any application in '{CommandLineState.LaunchPathArg}'.");
+ UserErrorDialog.CreateUserErrorDialog(UserError.ApplicationNotFound);
+ }
}
if (ConfigurationState.Instance.CheckUpdatesOnStart.Value && Updater.CanUpdate(false))
diff --git a/src/Ryujinx.Gtk3/UI/MainWindow.cs b/src/Ryujinx.Gtk3/UI/MainWindow.cs
index d1ca6ce6a..7f9eceb38 100644
--- a/src/Ryujinx.Gtk3/UI/MainWindow.cs
+++ b/src/Ryujinx.Gtk3/UI/MainWindow.cs
@@ -37,7 +37,9 @@ using Ryujinx.UI.Windows;
using Silk.NET.Vulkan;
using SPB.Graphics.Vulkan;
using System;
+using System.Collections.Generic;
using System.Diagnostics;
+using System.Globalization;
using System.IO;
using System.Reflection;
using System.Threading;
@@ -60,7 +62,6 @@ namespace Ryujinx.UI
private WindowsMultimediaTimerResolution _windowsMultimediaTimerResolution;
- private readonly ApplicationLibrary _applicationLibrary;
private readonly GtkHostUIHandler _uiHandler;
private readonly AutoResetEvent _deviceExitStatus;
private readonly ListStore _tableStore;
@@ -69,11 +70,12 @@ namespace Ryujinx.UI
private bool _gameLoaded;
private bool _ending;
- private string _currentEmulatedGamePath = null;
+ private ApplicationData _currentApplicationData = null;
private string _lastScannedAmiiboId = "";
private bool _lastScannedAmiiboShowAll = false;
+ public readonly ApplicationLibrary ApplicationLibrary;
public RendererWidgetBase RendererWidget;
public InputManager InputManager;
@@ -180,8 +182,12 @@ namespace Ryujinx.UI
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, CommandLineState.Profile);
_userChannelPersistence = new UserChannelPersistence();
+ IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
+ ? IntegrityCheckLevel.ErrorOnInvalid
+ : IntegrityCheckLevel.None;
+
// Instantiate GUI objects.
- _applicationLibrary = new ApplicationLibrary(_virtualFileSystem);
+ ApplicationLibrary = new ApplicationLibrary(_virtualFileSystem, checkLevel);
_uiHandler = new GtkHostUIHandler(this);
_deviceExitStatus = new AutoResetEvent(false);
@@ -190,8 +196,8 @@ namespace Ryujinx.UI
FocusInEvent += MainWindow_FocusInEvent;
FocusOutEvent += MainWindow_FocusOutEvent;
- _applicationLibrary.ApplicationAdded += Application_Added;
- _applicationLibrary.ApplicationCountUpdated += ApplicationCount_Updated;
+ ApplicationLibrary.ApplicationAdded += Application_Added;
+ ApplicationLibrary.ApplicationCountUpdated += ApplicationCount_Updated;
_fileMenu.StateChanged += FileMenu_StateChanged;
_actionMenu.StateChanged += ActionMenu_StateChanged;
@@ -732,7 +738,7 @@ namespace Ryujinx.UI
Thread applicationLibraryThread = new(() =>
{
- _applicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs, ConfigurationState.Instance.System.Language);
+ ApplicationLibrary.LoadApplications(ConfigurationState.Instance.UI.GameDirs, ConfigurationState.Instance.System.Language);
_updatingGameTable = false;
})
@@ -783,7 +789,7 @@ namespace Ryujinx.UI
}
}
- private bool LoadApplication(string path, bool isFirmwareTitle)
+ private bool LoadApplication(string path, ulong applicationId, bool isFirmwareTitle)
{
SystemVersion firmwareVersion = _contentManager.GetCurrentFirmwareVersion();
@@ -857,7 +863,7 @@ namespace Ryujinx.UI
case ".xci":
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
- return _emulationContext.LoadXci(path);
+ return _emulationContext.LoadXci(path, applicationId);
case ".nca":
Logger.Info?.Print(LogClass.Application, "Loading as NCA.");
@@ -866,7 +872,7 @@ namespace Ryujinx.UI
case ".pfs0":
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
- return _emulationContext.LoadNsp(path);
+ return _emulationContext.LoadNsp(path, applicationId);
default:
Logger.Info?.Print(LogClass.Application, "Loading as Homebrew.");
try
@@ -887,7 +893,7 @@ namespace Ryujinx.UI
return false;
}
- public void RunApplication(string path, bool startFullscreen = false)
+ public void RunApplication(ApplicationData application, bool startFullscreen = false)
{
if (_gameLoaded)
{
@@ -909,14 +915,14 @@ namespace Ryujinx.UI
bool isFirmwareTitle = false;
- if (path.StartsWith("@SystemContent"))
+ if (application.Path.StartsWith("@SystemContent"))
{
- path = VirtualFileSystem.SwitchPathToSystemPath(path);
+ application.Path = VirtualFileSystem.SwitchPathToSystemPath(application.Path);
isFirmwareTitle = true;
}
- if (!LoadApplication(path, isFirmwareTitle))
+ if (!LoadApplication(application.Path, application.Id, isFirmwareTitle))
{
_emulationContext.Dispose();
SwitchToGameTable();
@@ -926,7 +932,7 @@ namespace Ryujinx.UI
SetupProgressUIHandlers();
- _currentEmulatedGamePath = path;
+ _currentApplicationData = application;
_deviceExitStatus.Reset();
@@ -1165,7 +1171,7 @@ namespace Ryujinx.UI
_tableStore.AppendValues(
args.AppData.Favorite,
new Gdk.Pixbuf(args.AppData.Icon, 75, 75),
- $"{args.AppData.TitleName}\n{args.AppData.TitleId.ToUpper()}",
+ $"{args.AppData.Name}\n{args.AppData.IdString.ToUpper()}",
args.AppData.Developer,
args.AppData.Version,
args.AppData.TimePlayedString,
@@ -1253,9 +1259,22 @@ namespace Ryujinx.UI
{
_gameTableSelection.GetSelected(out TreeIter treeIter);
- string path = (string)_tableStore.GetValue(treeIter, 9);
+ ApplicationData application = new()
+ {
+ Favorite = (bool)_tableStore.GetValue(treeIter, 0),
+ Name = ((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[0],
+ Id = ulong.Parse(((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[1], NumberStyles.HexNumber),
+ Developer = (string)_tableStore.GetValue(treeIter, 3),
+ Version = (string)_tableStore.GetValue(treeIter, 4),
+ TimePlayed = ValueFormatUtils.ParseTimeSpan((string)_tableStore.GetValue(treeIter, 5)),
+ LastPlayed = ValueFormatUtils.ParseDateTime((string)_tableStore.GetValue(treeIter, 6)),
+ FileExtension = (string)_tableStore.GetValue(treeIter, 7),
+ FileSize = ValueFormatUtils.ParseFileSize((string)_tableStore.GetValue(treeIter, 8)),
+ Path = (string)_tableStore.GetValue(treeIter, 9),
+ ControlHolder = (BlitStruct)_tableStore.GetValue(treeIter, 10),
+ };
- RunApplication(path);
+ RunApplication(application);
}
private void VSyncStatus_Clicked(object sender, ButtonReleaseEventArgs args)
@@ -1313,13 +1332,22 @@ namespace Ryujinx.UI
return;
}
- string titleFilePath = _tableStore.GetValue(treeIter, 9).ToString();
- string titleName = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[0];
- string titleId = _tableStore.GetValue(treeIter, 2).ToString().Split("\n")[1].ToLower();
+ ApplicationData application = new()
+ {
+ Favorite = (bool)_tableStore.GetValue(treeIter, 0),
+ Name = ((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[0],
+ Id = ulong.Parse(((string)_tableStore.GetValue(treeIter, 2)).Split('\n')[1], NumberStyles.HexNumber),
+ Developer = (string)_tableStore.GetValue(treeIter, 3),
+ Version = (string)_tableStore.GetValue(treeIter, 4),
+ TimePlayed = ValueFormatUtils.ParseTimeSpan((string)_tableStore.GetValue(treeIter, 5)),
+ LastPlayed = ValueFormatUtils.ParseDateTime((string)_tableStore.GetValue(treeIter, 6)),
+ FileExtension = (string)_tableStore.GetValue(treeIter, 7),
+ FileSize = ValueFormatUtils.ParseFileSize((string)_tableStore.GetValue(treeIter, 8)),
+ Path = (string)_tableStore.GetValue(treeIter, 9),
+ ControlHolder = (BlitStruct)_tableStore.GetValue(treeIter, 10),
+ };
- BlitStruct controlData = (BlitStruct)_tableStore.GetValue(treeIter, 10);
-
- _ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, titleFilePath, titleName, titleId, controlData);
+ _ = new GameTableContextMenu(this, _virtualFileSystem, _accountManager, _libHacHorizonManager.RyujinxClient, application);
}
private void Load_Application_File(object sender, EventArgs args)
@@ -1341,7 +1369,15 @@ namespace Ryujinx.UI
if (fileChooser.Run() == (int)ResponseType.Accept)
{
- RunApplication(fileChooser.Filename);
+ if (ApplicationLibrary.TryGetApplicationsFromFile(fileChooser.Filename,
+ out List applications))
+ {
+ RunApplication(applications[0]);
+ }
+ else
+ {
+ GtkDialog.CreateErrorDialog("No applications found in selected file.");
+ }
}
}
@@ -1351,7 +1387,13 @@ namespace Ryujinx.UI
if (fileChooser.Run() == (int)ResponseType.Accept)
{
- RunApplication(fileChooser.Filename);
+ ApplicationData applicationData = new()
+ {
+ Name = System.IO.Path.GetFileNameWithoutExtension(fileChooser.Filename),
+ Path = fileChooser.Filename,
+ };
+
+ RunApplication(applicationData);
}
}
@@ -1366,7 +1408,14 @@ namespace Ryujinx.UI
{
string contentPath = _contentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program);
- RunApplication(contentPath);
+ ApplicationData applicationData = new()
+ {
+ Name = "miiEdit",
+ Id = 0x0100000000001009ul,
+ Path = contentPath,
+ };
+
+ RunApplication(applicationData);
}
private void Open_Ryu_Folder(object sender, EventArgs args)
@@ -1646,13 +1695,13 @@ namespace Ryujinx.UI
{
_userChannelPersistence.ShouldRestart = false;
- RunApplication(_currentEmulatedGamePath);
+ RunApplication(_currentApplicationData);
}
else
{
// otherwise, clear state.
_userChannelPersistence = new UserChannelPersistence();
- _currentEmulatedGamePath = null;
+ _currentApplicationData = null;
_actionMenu.Sensitive = false;
_firmwareInstallFile.Sensitive = true;
_firmwareInstallDirectory.Sensitive = true;
@@ -1714,7 +1763,7 @@ namespace Ryujinx.UI
_emulationContext.Processes.ActiveApplication.ProgramId,
_emulationContext.Processes.ActiveApplication.ApplicationControlProperties
.Title[(int)_emulationContext.System.State.DesiredTitleLanguage].NameString.ToString(),
- _currentEmulatedGamePath);
+ _currentApplicationData.Path);
window.Destroyed += CheatWindow_Destroyed;
window.Show();
diff --git a/src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.cs b/src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.cs
index c8236223a..e37906d5b 100644
--- a/src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.cs
+++ b/src/Ryujinx.Gtk3/UI/Widgets/GameTableContextMenu.cs
@@ -16,6 +16,8 @@ using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
+using Ryujinx.HLE.Loaders.Processes.Extensions;
+using Ryujinx.HLE.Utilities;
using Ryujinx.UI.App.Common;
using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper;
@@ -23,7 +25,6 @@ using Ryujinx.UI.Windows;
using System;
using System.Buffers;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using System.Reflection;
using System.Threading;
@@ -36,17 +37,13 @@ namespace Ryujinx.UI.Widgets
private readonly VirtualFileSystem _virtualFileSystem;
private readonly AccountManager _accountManager;
private readonly HorizonClient _horizonClient;
- private readonly BlitStruct _controlData;
- private readonly string _titleFilePath;
- private readonly string _titleName;
- private readonly string _titleIdText;
- private readonly ulong _titleId;
+ private readonly ApplicationData _applicationData;
private MessageDialog _dialog;
private bool _cancel;
- public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, string titleFilePath, string titleName, string titleId, BlitStruct controlData)
+ public GameTableContextMenu(MainWindow parent, VirtualFileSystem virtualFileSystem, AccountManager accountManager, HorizonClient horizonClient, ApplicationData applicationData)
{
_parent = parent;
@@ -55,23 +52,22 @@ namespace Ryujinx.UI.Widgets
_virtualFileSystem = virtualFileSystem;
_accountManager = accountManager;
_horizonClient = horizonClient;
- _titleFilePath = titleFilePath;
- _titleName = titleName;
- _titleIdText = titleId;
- _controlData = controlData;
+ _applicationData = applicationData;
- if (!ulong.TryParse(_titleIdText, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out _titleId))
+ if (!_applicationData.ControlHolder.ByteSpan.IsZeros())
{
- GtkDialog.CreateErrorDialog("The selected game did not have a valid Title Id");
-
- return;
+ _openSaveUserDirMenuItem.Sensitive = _applicationData.ControlHolder.Value.UserAccountSaveDataSize > 0;
+ _openSaveDeviceDirMenuItem.Sensitive = _applicationData.ControlHolder.Value.DeviceSaveDataSize > 0;
+ _openSaveBcatDirMenuItem.Sensitive = _applicationData.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0;
+ }
+ else
+ {
+ _openSaveUserDirMenuItem.Sensitive = false;
+ _openSaveDeviceDirMenuItem.Sensitive = false;
+ _openSaveBcatDirMenuItem.Sensitive = false;
}
- _openSaveUserDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.UserAccountSaveDataSize > 0;
- _openSaveDeviceDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.DeviceSaveDataSize > 0;
- _openSaveBcatDirMenuItem.Sensitive = !Utilities.IsZeros(controlData.ByteSpan) && controlData.Value.BcatDeliveryCacheStorageSize > 0;
-
- string fileExt = System.IO.Path.GetExtension(_titleFilePath).ToLower();
+ string fileExt = System.IO.Path.GetExtension(_applicationData.Path).ToLower();
bool hasNca = fileExt == ".nca" || fileExt == ".nsp" || fileExt == ".pfs0" || fileExt == ".xci";
_extractRomFsMenuItem.Sensitive = hasNca;
@@ -137,7 +133,7 @@ namespace Ryujinx.UI.Widgets
private void OpenSaveDir(in SaveDataFilter saveDataFilter)
{
- if (!TryFindSaveData(_titleName, _titleId, _controlData, in saveDataFilter, out ulong saveDataId))
+ if (!TryFindSaveData(_applicationData.Name, _applicationData.Id, _applicationData.ControlHolder, in saveDataFilter, out ulong saveDataId))
{
return;
}
@@ -190,7 +186,7 @@ namespace Ryujinx.UI.Widgets
{
Title = "Ryujinx - NCA Section Extractor",
Icon = new Gdk.Pixbuf(Assembly.GetAssembly(typeof(ConfigurationState)), "Ryujinx.Gtk3.UI.Common.Resources.Logo_Ryujinx.png"),
- SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_titleFilePath)}...",
+ SecondaryText = $"Extracting {ncaSectionType} section from {System.IO.Path.GetFileName(_applicationData.Path)}...",
WindowPosition = WindowPosition.Center,
};
@@ -202,29 +198,16 @@ namespace Ryujinx.UI.Widgets
}
});
- using FileStream file = new(_titleFilePath, FileMode.Open, FileAccess.Read);
+ using FileStream file = new(_applicationData.Path, FileMode.Open, FileAccess.Read);
Nca mainNca = null;
Nca patchNca = null;
- if ((System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nsp") ||
- (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".pfs0") ||
- (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".xci"))
+ if ((System.IO.Path.GetExtension(_applicationData.Path).ToLower() == ".nsp") ||
+ (System.IO.Path.GetExtension(_applicationData.Path).ToLower() == ".pfs0") ||
+ (System.IO.Path.GetExtension(_applicationData.Path).ToLower() == ".xci"))
{
- IFileSystem pfs;
-
- if (System.IO.Path.GetExtension(_titleFilePath) == ".xci")
- {
- Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
-
- pfs = xci.OpenPartition(XciPartitionType.Secure);
- }
- else
- {
- var pfsTemp = new PartitionFileSystem();
- pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
- pfs = pfsTemp;
- }
+ IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(_applicationData.Path, _virtualFileSystem);
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
{
@@ -249,7 +232,7 @@ namespace Ryujinx.UI.Widgets
}
}
}
- else if (System.IO.Path.GetExtension(_titleFilePath).ToLower() == ".nca")
+ else if (System.IO.Path.GetExtension(_applicationData.Path).ToLower() == ".nca")
{
mainNca = new Nca(_virtualFileSystem.KeySet, file.AsStorage());
}
@@ -266,7 +249,11 @@ namespace Ryujinx.UI.Widgets
return;
}
- (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _);
+ IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
+ ? IntegrityCheckLevel.ErrorOnInvalid
+ : IntegrityCheckLevel.None;
+
+ (Nca updatePatchNca, _) = mainNca.GetUpdateData(_virtualFileSystem, checkLevel, programIndex, out _);
if (updatePatchNca != null)
{
@@ -460,44 +447,44 @@ namespace Ryujinx.UI.Widgets
private void OpenSaveUserDir_Clicked(object sender, EventArgs args)
{
var userId = new LibHac.Fs.UserId((ulong)_accountManager.LastOpenedUser.UserId.High, (ulong)_accountManager.LastOpenedUser.UserId.Low);
- var saveDataFilter = SaveDataFilter.Make(_titleId, saveType: default, userId, saveDataId: default, index: default);
+ var saveDataFilter = SaveDataFilter.Make(_applicationData.Id, saveType: default, userId, saveDataId: default, index: default);
OpenSaveDir(in saveDataFilter);
}
private void OpenSaveDeviceDir_Clicked(object sender, EventArgs args)
{
- var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Device, userId: default, saveDataId: default, index: default);
+ var saveDataFilter = SaveDataFilter.Make(_applicationData.Id, SaveDataType.Device, userId: default, saveDataId: default, index: default);
OpenSaveDir(in saveDataFilter);
}
private void OpenSaveBcatDir_Clicked(object sender, EventArgs args)
{
- var saveDataFilter = SaveDataFilter.Make(_titleId, SaveDataType.Bcat, userId: default, saveDataId: default, index: default);
+ var saveDataFilter = SaveDataFilter.Make(_applicationData.Id, SaveDataType.Bcat, userId: default, saveDataId: default, index: default);
OpenSaveDir(in saveDataFilter);
}
private void ManageTitleUpdates_Clicked(object sender, EventArgs args)
{
- new TitleUpdateWindow(_parent, _virtualFileSystem, _titleIdText, _titleName).Show();
+ new TitleUpdateWindow(_parent, _virtualFileSystem, _applicationData).Show();
}
private void ManageDlc_Clicked(object sender, EventArgs args)
{
- new DlcWindow(_virtualFileSystem, _titleIdText, _titleName).Show();
+ new DlcWindow(_virtualFileSystem, _applicationData.IdString, _applicationData).Show();
}
private void ManageCheats_Clicked(object sender, EventArgs args)
{
- new CheatWindow(_virtualFileSystem, _titleId, _titleName, _titleFilePath).Show();
+ new CheatWindow(_virtualFileSystem, _applicationData.Id, _applicationData.Name, _applicationData.Path).Show();
}
private void OpenTitleModDir_Clicked(object sender, EventArgs args)
{
string modsBasePath = ModLoader.GetModsBasePath();
- string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, _titleIdText);
+ string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, _applicationData.IdString);
OpenHelper.OpenFolder(titleModsPath);
}
@@ -505,7 +492,7 @@ namespace Ryujinx.UI.Widgets
private void OpenTitleSdModDir_Clicked(object sender, EventArgs args)
{
string sdModsBasePath = ModLoader.GetSdModsBasePath();
- string titleModsPath = ModLoader.GetApplicationDir(sdModsBasePath, _titleIdText);
+ string titleModsPath = ModLoader.GetApplicationDir(sdModsBasePath, _applicationData.IdString);
OpenHelper.OpenFolder(titleModsPath);
}
@@ -527,7 +514,7 @@ namespace Ryujinx.UI.Widgets
private void OpenPtcDir_Clicked(object sender, EventArgs args)
{
- string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu");
+ string ptcDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationData.IdString, "cache", "cpu");
string mainPath = System.IO.Path.Combine(ptcDir, "0");
string backupPath = System.IO.Path.Combine(ptcDir, "1");
@@ -544,7 +531,7 @@ namespace Ryujinx.UI.Widgets
private void OpenShaderCacheDir_Clicked(object sender, EventArgs args)
{
- string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader");
+ string shaderCacheDir = System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationData.IdString, "cache", "shader");
if (!Directory.Exists(shaderCacheDir))
{
@@ -556,10 +543,10 @@ namespace Ryujinx.UI.Widgets
private void PurgePtcCache_Clicked(object sender, EventArgs args)
{
- DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "0"));
- DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "cpu", "1"));
+ DirectoryInfo mainDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationData.IdString, "cache", "cpu", "0"));
+ DirectoryInfo backupDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationData.IdString, "cache", "cpu", "1"));
- MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to queue a PPTC rebuild on the next boot of:\n\n{_titleName}\n\nAre you sure you want to proceed?");
+ MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to queue a PPTC rebuild on the next boot of:\n\n{_applicationData.Name}\n\nAre you sure you want to proceed?");
List cacheFiles = new();
@@ -593,9 +580,9 @@ namespace Ryujinx.UI.Widgets
private void PurgeShaderCache_Clicked(object sender, EventArgs args)
{
- DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleIdText, "cache", "shader"));
+ DirectoryInfo shaderCacheDir = new(System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationData.IdString, "cache", "shader"));
- using MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n{_titleName}\n\nAre you sure you want to proceed?");
+ using MessageDialog warningDialog = GtkDialog.CreateConfirmationDialog("Warning", $"You are about to delete the shader cache for :\n\n{_applicationData.Name}\n\nAre you sure you want to proceed?");
List oldCacheDirectories = new();
List newCacheFiles = new();
@@ -637,8 +624,11 @@ namespace Ryujinx.UI.Widgets
private void CreateShortcut_Clicked(object sender, EventArgs args)
{
- byte[] appIcon = new ApplicationLibrary(_virtualFileSystem).GetApplicationIcon(_titleFilePath, ConfigurationState.Instance.System.Language);
- ShortcutHelper.CreateAppShortcut(_titleFilePath, _titleName, _titleIdText, appIcon);
+ IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
+ ? IntegrityCheckLevel.ErrorOnInvalid
+ : IntegrityCheckLevel.None;
+ byte[] appIcon = new ApplicationLibrary(_virtualFileSystem, checkLevel).GetApplicationIcon(_applicationData.Path, ConfigurationState.Instance.System.Language, _applicationData.Id);
+ ShortcutHelper.CreateAppShortcut(_applicationData.Path, _applicationData.Name, _applicationData.IdString, appIcon);
}
}
}
diff --git a/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs
index 96ed0723e..d9f01918f 100644
--- a/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs
+++ b/src/Ryujinx.Gtk3/UI/Windows/CheatWindow.cs
@@ -1,7 +1,9 @@
using Gtk;
+using LibHac.Tools.FsSystem;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
using Ryujinx.UI.App.Common;
+using Ryujinx.UI.Common.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
@@ -27,8 +29,13 @@ namespace Ryujinx.UI.Windows
private CheatWindow(Builder builder, VirtualFileSystem virtualFileSystem, ulong titleId, string titleName, string titlePath) : base(builder.GetRawOwnedObject("_cheatWindow"))
{
builder.Autoconnect(this);
+
+ IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
+ ? IntegrityCheckLevel.ErrorOnInvalid
+ : IntegrityCheckLevel.None;
+
_baseTitleInfoLabel.Text = $"Cheats Available for {titleName} [{titleId:X16}]";
- _buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetApplicationBuildId(virtualFileSystem, titlePath)}";
+ _buildIdTextView.Buffer.Text = $"BuildId: {ApplicationData.GetBuildId(virtualFileSystem, checkLevel, titlePath)}";
string modsBasePath = ModLoader.GetModsBasePath();
string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, titleId.ToString("X16"));
diff --git a/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs
index 388f11089..b69cc0032 100644
--- a/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs
+++ b/src/Ryujinx.Gtk3/UI/Windows/DlcWindow.cs
@@ -2,17 +2,21 @@ using Gtk;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
-using LibHac.FsSystem;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.Loaders.Processes.Extensions;
+using Ryujinx.HLE.Utilities;
+using Ryujinx.UI.App.Common;
using Ryujinx.UI.Widgets;
using System;
using System.Collections.Generic;
+using System.Globalization;
using System.IO;
+using System.Linq;
using GUI = Gtk.Builder.ObjectAttribute;
namespace Ryujinx.UI.Windows
@@ -20,7 +24,7 @@ namespace Ryujinx.UI.Windows
public class DlcWindow : Window
{
private readonly VirtualFileSystem _virtualFileSystem;
- private readonly string _titleId;
+ private readonly string _applicationId;
private readonly string _dlcJsonPath;
private readonly List _dlcContainerList;
@@ -32,16 +36,16 @@ namespace Ryujinx.UI.Windows
[GUI] TreeSelection _dlcTreeSelection;
#pragma warning restore CS0649, IDE0044
- public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Gtk3.UI.Windows.DlcWindow.glade"), virtualFileSystem, titleId, titleName) { }
+ public DlcWindow(VirtualFileSystem virtualFileSystem, string titleId, ApplicationData applicationData) : this(new Builder("Ryujinx.Gtk3.UI.Windows.DlcWindow.glade"), virtualFileSystem, titleId, applicationData) { }
- private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_dlcWindow"))
+ private DlcWindow(Builder builder, VirtualFileSystem virtualFileSystem, string applicationId, ApplicationData applicationData) : base(builder.GetRawOwnedObject("_dlcWindow"))
{
builder.Autoconnect(this);
- _titleId = titleId;
+ _applicationId = applicationId;
_virtualFileSystem = virtualFileSystem;
- _dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "dlc.json");
- _baseTitleInfoLabel.Text = $"DLC Available for {titleName} [{titleId.ToUpper()}]";
+ _dlcJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _applicationId, "dlc.json");
+ _baseTitleInfoLabel.Text = $"DLC Available for {applicationData.Name} [{applicationId.ToUpper()}]";
try
{
@@ -72,7 +76,7 @@ namespace Ryujinx.UI.Windows
};
_dlcTreeView.AppendColumn("Enabled", enableToggle, "active", 0);
- _dlcTreeView.AppendColumn("TitleId", new CellRendererText(), "text", 1);
+ _dlcTreeView.AppendColumn("ApplicationId", new CellRendererText(), "text", 1);
_dlcTreeView.AppendColumn("Path", new CellRendererText(), "text", 2);
foreach (DownloadableContentContainer dlcContainer in _dlcContainerList)
@@ -86,18 +90,18 @@ namespace Ryujinx.UI.Windows
bool areAllContentPacksEnabled = dlcContainer.DownloadableContentNcaList.TrueForAll((nca) => nca.Enabled);
TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(areAllContentPacksEnabled, "", dlcContainer.ContainerPath);
- using FileStream containerFile = File.OpenRead(dlcContainer.ContainerPath);
+ using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(dlcContainer.ContainerPath, _virtualFileSystem, false);
- PartitionFileSystem pfs = new();
- pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
-
- _virtualFileSystem.ImportTickets(pfs);
+ if (partitionFileSystem == null)
+ {
+ continue;
+ }
foreach (DownloadableContentNca dlcNca in dlcContainer.DownloadableContentNcaList)
{
using var ncaFile = new UniqueRef();
- pfs.OpenFile(ref ncaFile.Ref, dlcNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+ partitionFileSystem.OpenFile(ref ncaFile.Ref, dlcNca.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), dlcContainer.ContainerPath);
if (nca != null)
@@ -112,6 +116,9 @@ namespace Ryujinx.UI.Windows
TreeIter parentIter = ((TreeStore)_dlcTreeView.Model).AppendValues(false, "", $"(MISSING) {dlcContainer.ContainerPath}");
}
}
+
+ // NOTE: Try to load downloadable contents from PFS last to preserve enabled state.
+ AddDlc(applicationData.Path, true);
}
private Nca TryCreateNca(IStorage ncaStorage, string containerPath)
@@ -128,6 +135,52 @@ namespace Ryujinx.UI.Windows
return null;
}
+ private void AddDlc(string path, bool ignoreNotFound = false)
+ {
+ if (!File.Exists(path) || _dlcContainerList.Any(x => x.ContainerPath == path))
+ {
+ return;
+ }
+
+ using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(path, _virtualFileSystem);
+
+ bool containsDlc = false;
+
+ TreeIter? parentIter = null;
+
+ foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
+ {
+ using var ncaFile = new UniqueRef();
+
+ partitionFileSystem.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+
+ Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), path);
+
+ if (nca == null)
+ {
+ continue;
+ }
+
+ if (nca.Header.ContentType == NcaContentType.PublicData)
+ {
+ if (nca.GetProgramIdBase() != (ulong.Parse(_applicationId, NumberStyles.HexNumber) & ~0x1FFFUL))
+ {
+ continue;
+ }
+
+ parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", path);
+
+ ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath);
+ containsDlc = true;
+ }
+ }
+
+ if (!containsDlc && !ignoreNotFound)
+ {
+ GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!");
+ }
+ }
+
private void AddButton_Clicked(object sender, EventArgs args)
{
FileChooserNative fileChooser = new("Select DLC files", this, FileChooserAction.Open, "Add", "Cancel")
@@ -147,52 +200,7 @@ namespace Ryujinx.UI.Windows
{
foreach (string containerPath in fileChooser.Filenames)
{
- if (!File.Exists(containerPath))
- {
- return;
- }
-
- using FileStream containerFile = File.OpenRead(containerPath);
-
- PartitionFileSystem pfs = new();
- pfs.Initialize(containerFile.AsStorage()).ThrowIfFailure();
- bool containsDlc = false;
-
- _virtualFileSystem.ImportTickets(pfs);
-
- TreeIter? parentIter = null;
-
- foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
- {
- using var ncaFile = new UniqueRef();
-
- pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
-
- Nca nca = TryCreateNca(ncaFile.Get.AsStorage(), containerPath);
-
- if (nca == null)
- {
- continue;
- }
-
- if (nca.Header.ContentType == NcaContentType.PublicData)
- {
- if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000).ToString("x16") != _titleId)
- {
- break;
- }
-
- parentIter ??= ((TreeStore)_dlcTreeView.Model).AppendValues(true, "", containerPath);
-
- ((TreeStore)_dlcTreeView.Model).AppendValues(parentIter.Value, true, nca.Header.TitleId.ToString("X16"), fileEntry.FullPath);
- containsDlc = true;
- }
- }
-
- if (!containsDlc)
- {
- GtkDialog.CreateErrorDialog("The specified file does not contain DLC for the selected title!");
- }
+ AddDlc(containerPath);
}
}
diff --git a/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.cs b/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.cs
index 74b2330ee..3ac972ead 100644
--- a/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.cs
+++ b/src/Ryujinx.Gtk3/UI/Windows/TitleUpdateWindow.cs
@@ -2,14 +2,17 @@ using Gtk;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
-using LibHac.FsSystem;
+using LibHac.Ncm;
using LibHac.Ns;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.Loaders.Processes.Extensions;
+using Ryujinx.HLE.Utilities;
using Ryujinx.UI.App.Common;
+using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Widgets;
using System;
using System.Collections.Generic;
@@ -24,7 +27,7 @@ namespace Ryujinx.UI.Windows
{
private readonly MainWindow _parent;
private readonly VirtualFileSystem _virtualFileSystem;
- private readonly string _titleId;
+ private readonly ApplicationData _applicationData;
private readonly string _updateJsonPath;
private TitleUpdateMetadata _titleUpdateWindowData;
@@ -38,17 +41,17 @@ namespace Ryujinx.UI.Windows
[GUI] RadioButton _noUpdateRadioButton;
#pragma warning restore CS0649, IDE0044
- public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : this(new Builder("Ryujinx.Gtk3.UI.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, titleId, titleName) { }
+ public TitleUpdateWindow(MainWindow parent, VirtualFileSystem virtualFileSystem, ApplicationData applicationData) : this(new Builder("Ryujinx.Gtk3.UI.Windows.TitleUpdateWindow.glade"), parent, virtualFileSystem, applicationData) { }
- private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, string titleId, string titleName) : base(builder.GetRawOwnedObject("_titleUpdateWindow"))
+ private TitleUpdateWindow(Builder builder, MainWindow parent, VirtualFileSystem virtualFileSystem, ApplicationData applicationData) : base(builder.GetRawOwnedObject("_titleUpdateWindow"))
{
_parent = parent;
builder.Autoconnect(this);
- _titleId = titleId;
+ _applicationData = applicationData;
_virtualFileSystem = virtualFileSystem;
- _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, _titleId, "updates.json");
+ _updateJsonPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, applicationData.IdString, "updates.json");
_radioButtonToPathDictionary = new Dictionary();
try
@@ -64,7 +67,10 @@ namespace Ryujinx.UI.Windows
};
}
- _baseTitleInfoLabel.Text = $"Updates Available for {titleName} [{titleId.ToUpper()}]";
+ _baseTitleInfoLabel.Text = $"Updates Available for {applicationData.Name} [{applicationData.IdString}]";
+
+ // Try to get updates from PFS first
+ AddUpdate(_applicationData.Path, true);
foreach (string path in _titleUpdateWindowData.Paths)
{
@@ -84,46 +90,68 @@ namespace Ryujinx.UI.Windows
}
}
- private void AddUpdate(string path)
+ private void AddUpdate(string path, bool ignoreNotFound = false)
{
- if (File.Exists(path))
+ if (!File.Exists(path) || _radioButtonToPathDictionary.ContainsValue(path))
{
- using FileStream file = new(path, FileMode.Open, FileAccess.Read);
+ return;
+ }
- PartitionFileSystem nsp = new();
- nsp.Initialize(file.AsStorage()).ThrowIfFailure();
+ IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
+ ? IntegrityCheckLevel.ErrorOnInvalid
+ : IntegrityCheckLevel.None;
- try
+ try
+ {
+ using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(path, _virtualFileSystem);
+
+ Dictionary updates = pfs.GetContentData(ContentMetaType.Patch, _virtualFileSystem, checkLevel);
+
+ Nca patchNca = null;
+ Nca controlNca = null;
+
+ if (updates.TryGetValue(_applicationData.Id, out ContentMetaData update))
{
- (Nca patchNca, Nca controlNca) = ApplicationLibrary.GetGameUpdateDataFromPartition(_virtualFileSystem, nsp, _titleId, 0);
+ patchNca = update.GetNcaByType(_virtualFileSystem.KeySet, LibHac.Ncm.ContentType.Program);
+ controlNca = update.GetNcaByType(_virtualFileSystem.KeySet, LibHac.Ncm.ContentType.Control);
+ }
- if (controlNca != null && patchNca != null)
+ if (controlNca != null && patchNca != null)
+ {
+ ApplicationControlProperty controlData = new();
+
+ using var nacpFile = new UniqueRef();
+
+ controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
+ nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
+
+ string radioLabel = $"Version {controlData.DisplayVersionString.ToString()} - {path}";
+
+ if (System.IO.Path.GetExtension(path).ToLower() == ".xci")
{
- ApplicationControlProperty controlData = new();
-
- using var nacpFile = new UniqueRef();
-
- controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None).OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read).ThrowIfFailure();
- nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure();
-
- RadioButton radioButton = new($"Version {controlData.DisplayVersionString.ToString()} - {path}");
- radioButton.JoinGroup(_noUpdateRadioButton);
-
- _availableUpdatesBox.Add(radioButton);
- _radioButtonToPathDictionary.Add(radioButton, path);
-
- radioButton.Show();
- radioButton.Active = true;
+ radioLabel = "Bundled: " + radioLabel;
}
- else
+
+ RadioButton radioButton = new(radioLabel);
+ radioButton.JoinGroup(_noUpdateRadioButton);
+
+ _availableUpdatesBox.Add(radioButton);
+ _radioButtonToPathDictionary.Add(radioButton, path);
+
+ radioButton.Show();
+ radioButton.Active = true;
+ }
+ else
+ {
+ if (!ignoreNotFound)
{
GtkDialog.CreateErrorDialog("The specified file does not contain an update for the selected title!");
}
}
- catch (Exception exception)
- {
- GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {path}");
- }
+ }
+ catch (Exception exception)
+ {
+ GtkDialog.CreateErrorDialog($"{exception.Message}. Errored File: {path}");
}
}
diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs
index 3c34a886b..e6c0fce08 100644
--- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs
+++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs
@@ -14,6 +14,7 @@ using Ryujinx.Common.Utilities;
using Ryujinx.HLE.Exceptions;
using Ryujinx.HLE.HOS.Services.Ssl;
using Ryujinx.HLE.HOS.Services.Time;
+using Ryujinx.HLE.Utilities;
using System;
using System.Collections.Generic;
using System.IO;
@@ -184,41 +185,6 @@ namespace Ryujinx.HLE.FileSystem
}
}
- // fs must contain AOC nca files in its root
- public void AddAocData(IFileSystem fs, string containerPath, ulong aocBaseId, IntegrityCheckLevel integrityCheckLevel)
- {
- _virtualFileSystem.ImportTickets(fs);
-
- foreach (var ncaPath in fs.EnumerateEntries("*.cnmt.nca", SearchOptions.Default))
- {
- using var ncaFile = new UniqueRef();
-
- fs.OpenFile(ref ncaFile.Ref, ncaPath.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
- var nca = new Nca(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
- if (nca.Header.ContentType != NcaContentType.Meta)
- {
- Logger.Warning?.Print(LogClass.Application, $"{ncaPath} is not a valid metadata file");
-
- continue;
- }
-
- using var pfs0 = nca.OpenFileSystem(0, integrityCheckLevel);
- using var cnmtFile = new UniqueRef();
-
- pfs0.OpenFile(ref cnmtFile.Ref, pfs0.EnumerateEntries().Single().FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
-
- var cnmt = new Cnmt(cnmtFile.Get.AsStream());
- if (cnmt.Type != ContentMetaType.AddOnContent || (cnmt.TitleId & 0xFFFFFFFFFFFFE000) != aocBaseId)
- {
- continue;
- }
-
- string ncaId = Convert.ToHexString(cnmt.ContentEntries[0].NcaId).ToLower();
-
- AddAocItem(cnmt.TitleId, containerPath, $"/{ncaId}.nca", true);
- }
- }
-
public void AddAocItem(ulong titleId, string containerPath, string ncaPath, bool mergedToContainer = false)
{
// TODO: Check Aoc version.
@@ -232,11 +198,7 @@ namespace Ryujinx.HLE.FileSystem
if (!mergedToContainer)
{
- using FileStream fileStream = File.OpenRead(containerPath);
- using PartitionFileSystem partitionFileSystem = new();
- partitionFileSystem.Initialize(fileStream.AsStorage()).ThrowIfFailure();
-
- _virtualFileSystem.ImportTickets(partitionFileSystem);
+ using var pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(containerPath, _virtualFileSystem);
}
}
}
diff --git a/src/Ryujinx.HLE/FileSystem/ContentMetaData.cs b/src/Ryujinx.HLE/FileSystem/ContentMetaData.cs
new file mode 100644
index 000000000..aebcf7988
--- /dev/null
+++ b/src/Ryujinx.HLE/FileSystem/ContentMetaData.cs
@@ -0,0 +1,61 @@
+using LibHac.Common.Keys;
+using LibHac.Fs.Fsa;
+using LibHac.Ncm;
+using LibHac.Tools.FsSystem.NcaUtils;
+using LibHac.Tools.Ncm;
+using Ryujinx.HLE.Loaders.Processes.Extensions;
+using System;
+
+namespace Ryujinx.HLE.FileSystem
+{
+ ///
+ /// Thin wrapper around
+ ///
+ public class ContentMetaData
+ {
+ private readonly IFileSystem _pfs;
+ private readonly Cnmt _cnmt;
+
+ public ulong Id => _cnmt.TitleId;
+ public TitleVersion Version => _cnmt.TitleVersion;
+ public ContentMetaType Type => _cnmt.Type;
+ public ulong ApplicationId => _cnmt.ApplicationTitleId;
+ public ulong PatchId => _cnmt.PatchTitleId;
+ public TitleVersion RequiredSystemVersion => _cnmt.MinimumSystemVersion;
+ public TitleVersion RequiredApplicationVersion => _cnmt.MinimumApplicationVersion;
+ public byte[] Digest => _cnmt.Hash;
+
+ public ulong ProgramBaseId => Id & ~0x1FFFUL;
+ public bool IsSystemTitle => _cnmt.Type < ContentMetaType.Application;
+
+ public ContentMetaData(IFileSystem pfs, Cnmt cnmt)
+ {
+ _pfs = pfs;
+ _cnmt = cnmt;
+ }
+
+ public Nca GetNcaByType(KeySet keySet, ContentType type, int programIndex = 0)
+ {
+ // TODO: Replace this with a check for IdOffset as soon as LibHac supports it:
+ // && entry.IdOffset == programIndex
+
+ foreach (var entry in _cnmt.ContentEntries)
+ {
+ if (entry.Type != type)
+ {
+ continue;
+ }
+
+ string ncaId = BitConverter.ToString(entry.NcaId).Replace("-", null).ToLower();
+ Nca nca = _pfs.GetNca(keySet, $"/{ncaId}.nca");
+
+ if (nca.GetProgramIndex() == programIndex)
+ {
+ return nca;
+ }
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs
index 6c2415e46..6c2a19894 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs
@@ -3,7 +3,6 @@ using LibHac.FsSystem;
using LibHac.Loader;
using LibHac.Ncm;
using LibHac.Ns;
-using Ryujinx.HLE.HOS;
using Ryujinx.HLE.Loaders.Processes.Extensions;
namespace Ryujinx.HLE.Loaders.Processes
diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs
index e70fcb6fc..da5637209 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs
@@ -7,16 +7,25 @@ using LibHac.Ncm;
using LibHac.Ns;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
+using LibHac.Tools.Ncm;
+using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
+using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS;
+using Ryujinx.HLE.Utilities;
using System.IO;
using System.Linq;
using ApplicationId = LibHac.Ncm.ApplicationId;
+using ContentType = LibHac.Ncm.ContentType;
+using Path = System.IO.Path;
namespace Ryujinx.HLE.Loaders.Processes.Extensions
{
- static class NcaExtensions
+ public static class NcaExtensions
{
+ private static readonly TitleUpdateMetadataJsonSerializerContext _applicationSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
+
public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca)
{
// Extract RomFs and ExeFs from NCA.
@@ -47,7 +56,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
nacpData = controlNca.GetNacp(device);
}
- /* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" inexistant update.
+ /* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" non-existent update.
// Load program 0 control NCA as we are going to need it for display version.
(_, Nca updateProgram0ControlNca) = GetGameUpdateData(_device.Configuration.VirtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _);
@@ -86,6 +95,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
return processResult;
}
+ public static ulong GetProgramIdBase(this Nca nca)
+ {
+ return nca.Header.TitleId & ~0x1FFFUL;
+ }
+
public static int GetProgramIndex(this Nca nca)
{
return (int)(nca.Header.TitleId & 0xF);
@@ -96,6 +110,11 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
return nca.Header.ContentType == NcaContentType.Program;
}
+ public static bool IsMain(this Nca nca)
+ {
+ return nca.IsProgram() && !nca.IsPatch();
+ }
+
public static bool IsPatch(this Nca nca)
{
int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
@@ -108,6 +127,43 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
return nca.Header.ContentType == NcaContentType.Control;
}
+ public static (Nca, Nca) GetUpdateData(this Nca mainNca, VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel, int programIndex, out string updatePath)
+ {
+ updatePath = null;
+
+ // Load Update NCAs.
+ Nca updatePatchNca = null;
+ Nca updateControlNca = null;
+
+ // Clear the program index part.
+ ulong titleIdBase = mainNca.GetProgramIdBase();
+
+ // Load update information if exists.
+ string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "updates.json");
+ if (File.Exists(titleUpdateMetadataPath))
+ {
+ updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _applicationSerializerContext.TitleUpdateMetadata).Selected;
+ if (File.Exists(updatePath))
+ {
+ IFileSystem updatePartitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(updatePath, fileSystem);
+
+ foreach ((ulong applicationTitleId, ContentMetaData content) in updatePartitionFileSystem.GetContentData(ContentMetaType.Patch, fileSystem, checkLevel))
+ {
+ if ((applicationTitleId & ~0x1FFFUL) != titleIdBase)
+ {
+ continue;
+ }
+
+ updatePatchNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Program, programIndex);
+ updateControlNca = content.GetNcaByType(fileSystem.KeySet, ContentType.Control, programIndex);
+ break;
+ }
+ }
+ }
+
+ return (updatePatchNca, updateControlNca);
+ }
+
public static IFileSystem GetExeFs(this Nca nca, Switch device, Nca patchNca = null)
{
IFileSystem exeFs = null;
@@ -172,5 +228,31 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
return nacpData;
}
+
+ public static Cnmt GetCnmt(this Nca cnmtNca, IntegrityCheckLevel checkLevel, ContentMetaType metaType)
+ {
+ string path = $"/{metaType}_{cnmtNca.Header.TitleId:x16}.cnmt";
+ using var cnmtFile = new UniqueRef();
+
+ try
+ {
+ Result result = cnmtNca.OpenFileSystem(0, checkLevel)
+ .OpenFile(ref cnmtFile.Ref, path.ToU8Span(), OpenMode.Read);
+
+ if (result.IsSuccess())
+ {
+ return new Cnmt(cnmtFile.Release().AsStream());
+ }
+ }
+ catch (HorizonResultException ex)
+ {
+ if (!ResultFs.PathNotFound.Includes(ex.ResultValue))
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Failed get CNMT for '{cnmtNca.Header.TitleId:x16}' from NCA: {ex.Message}");
+ }
+ }
+
+ return null;
+ }
}
}
diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs
index e95b1b059..bee2572a8 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/PartitionFileSystemExtensions.cs
@@ -1,26 +1,58 @@
using LibHac.Common;
+using LibHac.Common.Keys;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
+using LibHac.Ncm;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
+using LibHac.Tools.Ncm;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
+using Ryujinx.HLE.FileSystem;
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
+using ContentType = LibHac.Ncm.ContentType;
namespace Ryujinx.HLE.Loaders.Processes.Extensions
{
public static class PartitionFileSystemExtensions
{
private static readonly DownloadableContentJsonSerializerContext _contentSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
- private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
- internal static (bool, ProcessResult) TryLoad(this PartitionFileSystemCore partitionFileSystem, Switch device, string path, out string errorMessage)
+ public static Dictionary GetContentData(this IFileSystem partitionFileSystem,
+ ContentMetaType contentType, VirtualFileSystem fileSystem, IntegrityCheckLevel checkLevel)
+ {
+ fileSystem.ImportTickets(partitionFileSystem);
+
+ var programs = new Dictionary();
+
+ foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.cnmt.nca"))
+ {
+ Cnmt cnmt = partitionFileSystem.GetNca(fileSystem.KeySet, fileEntry.FullPath).GetCnmt(checkLevel, contentType);
+
+ if (cnmt == null)
+ {
+ continue;
+ }
+
+ ContentMetaData content = new(partitionFileSystem, cnmt);
+
+ if (content.Type != contentType)
+ {
+ continue;
+ }
+
+ programs.TryAdd(content.ApplicationId, content);
+ }
+
+ return programs;
+ }
+
+ internal static (bool, ProcessResult) TryLoad(this PartitionFileSystemCore partitionFileSystem, Switch device, string path, ulong applicationId, out string errorMessage)
where TMetaData : PartitionFileSystemMetaCore, new()
where TFormat : IPartitionFileSystemFormat
where THeader : unmanaged, IPartitionFileSystemHeader
@@ -35,31 +67,22 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
try
{
- device.Configuration.VirtualFileSystem.ImportTickets(partitionFileSystem);
+ Dictionary applications = partitionFileSystem.GetContentData(ContentMetaType.Application, device.FileSystem, device.System.FsIntegrityCheckLevel);
- // TODO: To support multi-games container, this should use CNMT NCA instead.
- foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
+ if (applicationId == 0)
{
- Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath);
-
- if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index)
+ foreach ((ulong _, ContentMetaData content) in applications)
{
- continue;
- }
-
- if (nca.IsPatch())
- {
- patchNca = nca;
- }
- else if (nca.IsProgram())
- {
- mainNca = nca;
- }
- else if (nca.IsControl())
- {
- controlNca = nca;
+ mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index);
+ controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index);
+ break;
}
}
+ else if (applications.TryGetValue(applicationId, out ContentMetaData content))
+ {
+ mainNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Program, device.Configuration.UserChannelPersistence.Index);
+ controlNca = content.GetNcaByType(device.FileSystem.KeySet, ContentType.Control, device.Configuration.UserChannelPersistence.Index);
+ }
ProcessLoaderHelper.RegisterProgramMapInfo(device, partitionFileSystem).ThrowIfFailure();
}
@@ -79,54 +102,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
return (false, ProcessResult.Failed);
}
- // Load Update NCAs.
- Nca updatePatchNca = null;
- Nca updateControlNca = null;
-
- if (ulong.TryParse(mainNca.Header.TitleId.ToString("x16"), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase))
- {
- // Clear the program index part.
- titleIdBase &= ~0xFUL;
-
- // Load update information if exists.
- string titleUpdateMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json");
- if (File.Exists(titleUpdateMetadataPath))
- {
- string updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
- if (File.Exists(updatePath))
- {
- PartitionFileSystem updatePartitionFileSystem = new();
- updatePartitionFileSystem.Initialize(new FileStream(updatePath, FileMode.Open, FileAccess.Read).AsStorage()).ThrowIfFailure();
-
- device.Configuration.VirtualFileSystem.ImportTickets(updatePartitionFileSystem);
-
- // TODO: This should use CNMT NCA instead.
- foreach (DirectoryEntryEx fileEntry in updatePartitionFileSystem.EnumerateEntries("/", "*.nca"))
- {
- Nca nca = updatePartitionFileSystem.GetNca(device, fileEntry.FullPath);
-
- if (nca.GetProgramIndex() != device.Configuration.UserChannelPersistence.Index)
- {
- continue;
- }
-
- if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleIdBase.ToString("x16"))
- {
- break;
- }
-
- if (nca.IsProgram())
- {
- updatePatchNca = nca;
- }
- else if (nca.IsControl())
- {
- updateControlNca = nca;
- }
- }
- }
- }
- }
+ (Nca updatePatchNca, Nca updateControlNca) = mainNca.GetUpdateData(device.FileSystem, device.System.FsIntegrityCheckLevel, device.Configuration.UserChannelPersistence.Index, out string _);
if (updatePatchNca != null)
{
@@ -138,10 +114,8 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
controlNca = updateControlNca;
}
- // Load contained DownloadableContents.
// TODO: If we want to support multi-processes in future, we shouldn't clear AddOnContent data here.
device.Configuration.ContentManager.ClearAocData();
- device.Configuration.ContentManager.AddAocData(partitionFileSystem, path, mainNca.Header.TitleId, device.Configuration.FsIntegrityCheckLevel);
// Load DownloadableContents.
string addOnContentMetadataPath = System.IO.Path.Combine(AppDataManager.GamesDirPath, mainNca.Header.TitleId.ToString("x16"), "dlc.json");
@@ -153,9 +127,12 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
{
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
{
- if (File.Exists(downloadableContentContainer.ContainerPath) && downloadableContentNca.Enabled)
+ if (File.Exists(downloadableContentContainer.ContainerPath))
{
- device.Configuration.ContentManager.AddAocItem(downloadableContentNca.TitleId, downloadableContentContainer.ContainerPath, downloadableContentNca.FullPath);
+ if (downloadableContentNca.Enabled)
+ {
+ device.Configuration.ContentManager.AddAocItem(downloadableContentNca.TitleId, downloadableContentContainer.ContainerPath, downloadableContentNca.FullPath);
+ }
}
else
{
@@ -168,18 +145,18 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
return (true, mainNca.Load(device, patchNca, controlNca));
}
- errorMessage = "Unable to load: Could not find Main NCA";
+ errorMessage = $"Unable to load: Could not find Main NCA for title \"{applicationId:X16}\"";
return (false, ProcessResult.Failed);
}
- public static Nca GetNca(this IFileSystem fileSystem, Switch device, string path)
+ public static Nca GetNca(this IFileSystem fileSystem, KeySet keySet, string path)
{
using var ncaFile = new UniqueRef();
fileSystem.OpenFile(ref ncaFile.Ref, path.ToU8Span(), OpenMode.Read).ThrowIfFailure();
- return new Nca(device.Configuration.VirtualFileSystem.KeySet, ncaFile.Release().AsStorage());
+ return new Nca(keySet, ncaFile.Release().AsStorage());
}
}
}
diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
index e5056c89a..12d9c8bd9 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
@@ -32,7 +32,7 @@ namespace Ryujinx.HLE.Loaders.Processes
_processesByPid = new ConcurrentDictionary();
}
- public bool LoadXci(string path)
+ public bool LoadXci(string path, ulong applicationId)
{
FileStream stream = new(path, FileMode.Open, FileAccess.Read);
Xci xci = new(_device.Configuration.VirtualFileSystem.KeySet, stream.AsStorage());
@@ -44,7 +44,7 @@ namespace Ryujinx.HLE.Loaders.Processes
return false;
}
- (bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, out string errorMessage);
+ (bool success, ProcessResult processResult) = xci.OpenPartition(XciPartitionType.Secure).TryLoad(_device, path, applicationId, out string errorMessage);
if (!success)
{
@@ -66,13 +66,13 @@ namespace Ryujinx.HLE.Loaders.Processes
return false;
}
- public bool LoadNsp(string path)
+ public bool LoadNsp(string path, ulong applicationId)
{
FileStream file = new(path, FileMode.Open, FileAccess.Read);
PartitionFileSystem partitionFileSystem = new();
partitionFileSystem.Initialize(file.AsStorage()).ThrowIfFailure();
- (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, out string errorMessage);
+ (bool success, ProcessResult processResult) = partitionFileSystem.TryLoad(_device, path, applicationId, out string errorMessage);
if (processResult.ProcessId == 0)
{
diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs
index fe2aaac6d..cf4eb416e 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs
+++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs
@@ -43,15 +43,14 @@ namespace Ryujinx.HLE.Loaders.Processes
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
{
- Nca nca = partitionFileSystem.GetNca(device, fileEntry.FullPath);
+ Nca nca = partitionFileSystem.GetNca(device.FileSystem.KeySet, fileEntry.FullPath);
- if (!nca.IsProgram() && nca.IsPatch())
+ if (!nca.IsProgram())
{
continue;
}
- ulong currentProgramId = nca.Header.TitleId;
- ulong currentMainProgramId = currentProgramId & ~0xFFFul;
+ ulong currentMainProgramId = nca.GetProgramIdBase();
if (applicationId == 0 && currentMainProgramId != 0)
{
@@ -68,7 +67,7 @@ namespace Ryujinx.HLE.Loaders.Processes
break;
}
- hasIndex[(int)(currentProgramId & 0xF)] = true;
+ hasIndex[nca.GetProgramIndex()] = true;
}
if (programCount == 0)
diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs
index 81c3ab473..9dfc69892 100644
--- a/src/Ryujinx.HLE/Switch.cs
+++ b/src/Ryujinx.HLE/Switch.cs
@@ -73,9 +73,9 @@ namespace Ryujinx.HLE
return Processes.LoadUnpackedNca(exeFsDir, romFsFile);
}
- public bool LoadXci(string xciFile)
+ public bool LoadXci(string xciFile, ulong applicationId = 0)
{
- return Processes.LoadXci(xciFile);
+ return Processes.LoadXci(xciFile, applicationId);
}
public bool LoadNca(string ncaFile)
@@ -83,9 +83,9 @@ namespace Ryujinx.HLE
return Processes.LoadNca(ncaFile);
}
- public bool LoadNsp(string nspFile)
+ public bool LoadNsp(string nspFile, ulong applicationId = 0)
{
- return Processes.LoadNsp(nspFile);
+ return Processes.LoadNsp(nspFile, applicationId);
}
public bool LoadProgram(string fileName)
diff --git a/src/Ryujinx.HLE/Utilities/PartitionFileSystemUtils.cs b/src/Ryujinx.HLE/Utilities/PartitionFileSystemUtils.cs
new file mode 100644
index 000000000..3c4ce0850
--- /dev/null
+++ b/src/Ryujinx.HLE/Utilities/PartitionFileSystemUtils.cs
@@ -0,0 +1,45 @@
+using LibHac;
+using LibHac.Fs.Fsa;
+using LibHac.FsSystem;
+using LibHac.Tools.Fs;
+using LibHac.Tools.FsSystem;
+using Ryujinx.HLE.FileSystem;
+using System.IO;
+
+namespace Ryujinx.HLE.Utilities
+{
+ public static class PartitionFileSystemUtils
+ {
+ public static IFileSystem OpenApplicationFileSystem(string path, VirtualFileSystem fileSystem, bool throwOnFailure = true)
+ {
+ FileStream file = File.OpenRead(path);
+
+ IFileSystem partitionFileSystem;
+
+ if (Path.GetExtension(path).ToLower() == ".xci")
+ {
+ partitionFileSystem = new Xci(fileSystem.KeySet, file.AsStorage()).OpenPartition(XciPartitionType.Secure);
+ }
+ else
+ {
+ var pfsTemp = new PartitionFileSystem();
+ Result initResult = pfsTemp.Initialize(file.AsStorage());
+
+ if (throwOnFailure)
+ {
+ initResult.ThrowIfFailure();
+ }
+ else if (initResult.IsFailure())
+ {
+ return null;
+ }
+
+ partitionFileSystem = pfsTemp;
+ }
+
+ fileSystem.ImportTickets(partitionFileSystem);
+
+ return partitionFileSystem;
+ }
+ }
+}
diff --git a/src/Ryujinx.UI.Common/App/ApplicationData.cs b/src/Ryujinx.UI.Common/App/ApplicationData.cs
index 13c05655b..7108defc3 100644
--- a/src/Ryujinx.UI.Common/App/ApplicationData.cs
+++ b/src/Ryujinx.UI.Common/App/ApplicationData.cs
@@ -9,9 +9,11 @@ using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.UI.Common.Helper;
using System;
using System.IO;
+using System.Text.Json.Serialization;
namespace Ryujinx.UI.App.Common
{
@@ -19,10 +21,10 @@ namespace Ryujinx.UI.App.Common
{
public bool Favorite { get; set; }
public byte[] Icon { get; set; }
- public string TitleName { get; set; }
- public string TitleId { get; set; }
- public string Developer { get; set; }
- public string Version { get; set; }
+ public string Name { get; set; } = "Unknown";
+ public ulong Id { get; set; }
+ public string Developer { get; set; } = "Unknown";
+ public string Version { get; set; } = "0";
public TimeSpan TimePlayed { get; set; }
public DateTime? LastPlayed { get; set; }
public string FileExtension { get; set; }
@@ -36,7 +38,11 @@ namespace Ryujinx.UI.App.Common
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
- public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath)
+ [JsonIgnore] public string IdString => Id.ToString("x16");
+
+ [JsonIgnore] public ulong IdBase => Id & ~0x1FFFUL;
+
+ public static string GetBuildId(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel, string titleFilePath)
{
using FileStream file = new(titleFilePath, FileMode.Open, FileAccess.Read);
@@ -105,7 +111,7 @@ namespace Ryujinx.UI.App.Common
return string.Empty;
}
- (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), 0, out _);
+ (Nca updatePatchNca, _) = mainNca.GetUpdateData(virtualFileSystem, checkLevel, 0, out string _);
if (updatePatchNca != null)
{
diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
index 176011dde..ef3826cfa 100644
--- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
+++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs
@@ -4,28 +4,29 @@ using LibHac.Common.Keys;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSystem;
+using LibHac.Ncm;
using LibHac.Ns;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
-using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.Loaders.Npdm;
+using Ryujinx.HLE.Loaders.Processes.Extensions;
using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Configuration.System;
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.Json;
using System.Threading;
+using ContentType = LibHac.Ncm.ContentType;
using Path = System.IO.Path;
using TimeSpan = System.TimeSpan;
@@ -43,15 +44,16 @@ namespace Ryujinx.UI.App.Common
private readonly byte[] _nsoIcon;
private readonly VirtualFileSystem _virtualFileSystem;
+ private readonly IntegrityCheckLevel _checkLevel;
private Language _desiredTitleLanguage;
private CancellationTokenSource _cancellationToken;
private static readonly ApplicationJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
- private static readonly TitleUpdateMetadataJsonSerializerContext _titleSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
- public ApplicationLibrary(VirtualFileSystem virtualFileSystem)
+ public ApplicationLibrary(VirtualFileSystem virtualFileSystem, IntegrityCheckLevel checkLevel)
{
_virtualFileSystem = virtualFileSystem;
+ _checkLevel = checkLevel;
_nspIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_NSP.png");
_xciIcon = GetResourceBytes("Ryujinx.UI.Common.Resources.Icon_XCI.png");
@@ -70,6 +72,400 @@ namespace Ryujinx.UI.App.Common
return resourceByteArray;
}
+ private ApplicationData GetApplicationFromExeFs(PartitionFileSystem pfs, string filePath)
+ {
+ ApplicationData data = new()
+ {
+ Icon = _nspIcon,
+ };
+
+ using UniqueRef npdmFile = new();
+
+ try
+ {
+ Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read);
+
+ if (ResultFs.PathNotFound.Includes(result))
+ {
+ Npdm npdm = new(npdmFile.Get.AsStream());
+
+ data.Name = npdm.TitleName;
+ data.Id = npdm.Aci0.TitleId;
+ }
+
+ return data;
+ }
+ catch (Exception exception)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception.Message}");
+
+ return null;
+ }
+ }
+
+ private ApplicationData GetApplicationFromNsp(PartitionFileSystem pfs, string filePath)
+ {
+ bool isExeFs = false;
+
+ // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application.
+ bool hasMainNca = false;
+
+ foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
+ {
+ if (Path.GetExtension(fileEntry.FullPath)?.ToLower() == ".nca")
+ {
+ using UniqueRef ncaFile = new();
+
+ try
+ {
+ pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+
+ Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
+ int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
+
+ // Some main NCAs don't have a data partition, so check if the partition exists before opening it
+ if (nca.Header.ContentType == NcaContentType.Program &&
+ !(nca.SectionExists(NcaSectionType.Data) &&
+ nca.Header.GetFsHeader(dataIndex).IsPatchSection()))
+ {
+ hasMainNca = true;
+
+ break;
+ }
+ }
+ catch (Exception exception)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"Encountered an error while trying to load applications from file '{filePath}': {exception}");
+
+ return null;
+ }
+ }
+ else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main")
+ {
+ isExeFs = true;
+ }
+ }
+
+ if (hasMainNca)
+ {
+ List applications = GetApplicationsFromPfs(pfs, filePath);
+
+ switch (applications.Count)
+ {
+ case 1:
+ return applications[0];
+ case >= 1:
+ Logger.Warning?.Print(LogClass.Application, $"File '{filePath}' contains more applications than expected: {applications.Count}");
+ return applications[0];
+ default:
+ return null;
+ }
+ }
+
+ if (isExeFs)
+ {
+ return GetApplicationFromExeFs(pfs, filePath);
+ }
+
+ return null;
+ }
+
+ private List GetApplicationsFromPfs(IFileSystem pfs, string filePath)
+ {
+ var applications = new List();
+ string extension = Path.GetExtension(filePath).ToLower();
+
+ foreach ((ulong titleId, ContentMetaData content) in pfs.GetContentData(ContentMetaType.Application, _virtualFileSystem, _checkLevel))
+ {
+ ApplicationData applicationData = new()
+ {
+ Id = titleId,
+ Path = filePath,
+ };
+
+ try
+ {
+ Nca mainNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Program);
+ Nca controlNca = content.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control);
+
+ BlitStruct controlHolder = new(1);
+
+ IFileSystem controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
+
+ // Check if there is an update available.
+ if (IsUpdateApplied(mainNca, out IFileSystem updatedControlFs))
+ {
+ // Replace the original ControlFs by the updated one.
+ controlFs = updatedControlFs;
+ }
+
+ ReadControlData(controlFs, controlHolder.ByteSpan);
+
+ GetApplicationInformation(ref controlHolder.Value, ref applicationData);
+
+ // Read the icon from the ControlFS and store it as a byte array
+ try
+ {
+ using UniqueRef icon = new();
+
+ controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
+
+ using MemoryStream stream = new();
+
+ icon.Get.AsStream().CopyTo(stream);
+ applicationData.Icon = stream.ToArray();
+ }
+ catch (HorizonResultException)
+ {
+ foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
+ {
+ if (entry.Name == "control.nacp")
+ {
+ continue;
+ }
+
+ using var icon = new UniqueRef();
+
+ controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
+
+ using MemoryStream stream = new();
+
+ icon.Get.AsStream().CopyTo(stream);
+ applicationData.Icon = stream.ToArray();
+
+ if (applicationData.Icon != null)
+ {
+ break;
+ }
+ }
+
+ applicationData.Icon ??= extension == ".xci" ? _xciIcon : _nspIcon;
+ }
+
+ applicationData.ControlHolder = controlHolder;
+
+ applications.Add(applicationData);
+ }
+ catch (MissingKeyException exception)
+ {
+ applicationData.Icon = extension == ".xci" ? _xciIcon : _nspIcon;
+
+ Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}");
+ }
+ catch (InvalidDataException)
+ {
+ applicationData.Icon = extension == ".xci" ? _xciIcon : _nspIcon;
+
+ Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {filePath}");
+ }
+ catch (Exception exception)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{filePath}' Error: {exception}");
+ }
+ }
+
+ return applications;
+ }
+
+ public bool TryGetApplicationsFromFile(string applicationPath, out List applications)
+ {
+ applications = [];
+
+ long fileSize = new FileInfo(applicationPath).Length;
+
+ BlitStruct controlHolder = new(1);
+
+ try
+ {
+ string extension = Path.GetExtension(applicationPath).ToLower();
+
+ using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read);
+
+ switch (extension)
+ {
+ case ".xci":
+ {
+ Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
+
+ applications = GetApplicationsFromPfs(xci.OpenPartition(XciPartitionType.Secure), applicationPath);
+
+ if (applications.Count == 0)
+ {
+ return false;
+ }
+
+ break;
+ }
+ case ".nsp":
+ case ".pfs0":
+ {
+ var pfs = new PartitionFileSystem();
+ pfs.Initialize(file.AsStorage()).ThrowIfFailure();
+
+ ApplicationData result = GetApplicationFromNsp(pfs, applicationPath);
+
+ if (result == null)
+ {
+ return false;
+ }
+
+ applications.Add(result);
+
+ break;
+ }
+ case ".nro":
+ {
+ BinaryReader reader = new(file);
+ ApplicationData application = new();
+
+ try
+ {
+ file.Seek(24, SeekOrigin.Begin);
+
+ int assetOffset = reader.ReadInt32();
+
+ if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET")
+ {
+ byte[] iconSectionInfo = Read(assetOffset + 8, 0x10);
+
+ long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0);
+ long iconSize = BitConverter.ToInt64(iconSectionInfo, 8);
+
+ ulong nacpOffset = reader.ReadUInt64();
+ ulong nacpSize = reader.ReadUInt64();
+
+ // Reads and stores game icon as byte array
+ if (iconSize > 0)
+ {
+ application.Icon = Read(assetOffset + iconOffset, (int)iconSize);
+ }
+ else
+ {
+ application.Icon = _nroIcon;
+ }
+
+ // Read the NACP data
+ Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan);
+
+ GetApplicationInformation(ref controlHolder.Value, ref application);
+ }
+ else
+ {
+ application.Icon = _nroIcon;
+ application.Name = Path.GetFileNameWithoutExtension(applicationPath);
+ }
+
+ application.ControlHolder = controlHolder;
+ applications.Add(application);
+ }
+ catch
+ {
+ Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}");
+
+ return false;
+ }
+
+ break;
+
+ byte[] Read(long position, int size)
+ {
+ file.Seek(position, SeekOrigin.Begin);
+
+ return reader.ReadBytes(size);
+ }
+ }
+ case ".nca":
+ {
+ try
+ {
+ ApplicationData application = new();
+
+ Nca nca = new(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage());
+
+ if (!nca.IsProgram() || nca.IsPatch())
+ {
+ return false;
+ }
+
+ application.Icon = _ncaIcon;
+ application.Name = Path.GetFileNameWithoutExtension(applicationPath);
+ application.ControlHolder = controlHolder;
+
+ applications.Add(application);
+ }
+ catch (InvalidDataException)
+ {
+ Logger.Warning?.Print(LogClass.Application, $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}");
+ }
+ catch
+ {
+ Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}");
+
+ return false;
+ }
+
+ break;
+ }
+ // If its an NSO we just set defaults
+ case ".nso":
+ {
+ ApplicationData application = new()
+ {
+ Icon = _nsoIcon,
+ Name = Path.GetFileNameWithoutExtension(applicationPath),
+ };
+
+ applications.Add(application);
+ break;
+ }
+ }
+ }
+ catch (IOException exception)
+ {
+ Logger.Warning?.Print(LogClass.Application, exception.Message);
+
+ return false;
+ }
+
+ foreach (var data in applications)
+ {
+ ApplicationMetadata appMetadata = LoadAndSaveMetaData(data.IdString, appMetadata =>
+ {
+ appMetadata.Title = data.Name;
+
+ // Only do the migration if time_played has a value and timespan_played hasn't been updated yet.
+ if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero)
+ {
+ appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld);
+ appMetadata.TimePlayedOld = default;
+ }
+
+ // Only do the migration if last_played has a value and last_played_utc doesn't exist yet.
+ if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue)
+ {
+ // Migrate from string-based last_played to DateTime-based last_played_utc.
+ if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed))
+ {
+ appMetadata.LastPlayed = lastPlayedOldParsed;
+
+ // Migration successful: deleting last_played from the metadata file.
+ appMetadata.LastPlayedOld = default;
+ }
+
+ }
+ });
+
+ data.Favorite = appMetadata.Favorite;
+ data.TimePlayed = appMetadata.TimePlayed;
+ data.LastPlayed = appMetadata.LastPlayed;
+ data.FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper();
+ data.FileSize = fileSize;
+ data.Path = applicationPath;
+ }
+
+ return true;
+ }
+
public void CancelLoading()
{
_cancellationToken?.Cancel();
@@ -93,7 +489,7 @@ namespace Ryujinx.UI.App.Common
_cancellationToken = new CancellationTokenSource();
// Builds the applications list with paths to found applications
- List applications = new();
+ List applicationPaths = new();
try
{
@@ -137,14 +533,7 @@ namespace Ryujinx.UI.App.Common
if (!fileInfo.Attributes.HasFlag(FileAttributes.Hidden) && extension is ".nsp" or ".pfs0" or ".xci" or ".nca" or ".nro" or ".nso")
{
var fullPath = fileInfo.ResolveLinkTarget(true)?.FullName ?? fileInfo.FullName;
-
- if (!File.Exists(fullPath))
- {
- Logger.Warning?.Print(LogClass.Application, $"Skipping invalid symlink: {fileInfo.FullName}");
- continue;
- }
-
- applications.Add(fullPath);
+ applicationPaths.Add(fullPath);
numApplicationsFound++;
}
}
@@ -156,328 +545,35 @@ namespace Ryujinx.UI.App.Common
}
// Loops through applications list, creating a struct and then firing an event containing the struct for each application
- foreach (string applicationPath in applications)
+ foreach (string applicationPath in applicationPaths)
{
if (_cancellationToken.Token.IsCancellationRequested)
{
return;
}
- long fileSize = new FileInfo(applicationPath).Length;
- string titleName = "Unknown";
- string titleId = "0000000000000000";
- string developer = "Unknown";
- string version = "0";
- byte[] applicationIcon = null;
-
- BlitStruct controlHolder = new(1);
-
- try
+ if (TryGetApplicationsFromFile(applicationPath, out List applications))
{
- string extension = Path.GetExtension(applicationPath).ToLower();
-
- using FileStream file = new(applicationPath, FileMode.Open, FileAccess.Read);
-
- if (extension == ".nsp" || extension == ".pfs0" || extension == ".xci")
+ foreach (var application in applications)
{
- try
+ OnApplicationAdded(new ApplicationAddedEventArgs
{
- IFileSystem pfs;
-
- bool isExeFs = false;
-
- if (extension == ".xci")
- {
- Xci xci = new(_virtualFileSystem.KeySet, file.AsStorage());
-
- pfs = xci.OpenPartition(XciPartitionType.Secure);
- }
- else
- {
- var pfsTemp = new PartitionFileSystem();
- pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
- pfs = pfsTemp;
-
- // If the NSP doesn't have a main NCA, decrement the number of applications found and then continue to the next application.
- bool hasMainNca = false;
-
- foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*"))
- {
- if (Path.GetExtension(fileEntry.FullPath).ToLower() == ".nca")
- {
- using UniqueRef ncaFile = new();
-
- pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
-
- Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
- int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
-
- // Some main NCAs don't have a data partition, so check if the partition exists before opening it
- if (nca.Header.ContentType == NcaContentType.Program && !(nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()))
- {
- hasMainNca = true;
-
- break;
- }
- }
- else if (Path.GetFileNameWithoutExtension(fileEntry.FullPath) == "main")
- {
- isExeFs = true;
- }
- }
-
- if (!hasMainNca && !isExeFs)
- {
- numApplicationsFound--;
-
- continue;
- }
- }
-
- if (isExeFs)
- {
- applicationIcon = _nspIcon;
-
- using UniqueRef npdmFile = new();
-
- Result result = pfs.OpenFile(ref npdmFile.Ref, "/main.npdm".ToU8Span(), OpenMode.Read);
-
- if (ResultFs.PathNotFound.Includes(result))
- {
- Npdm npdm = new(npdmFile.Get.AsStream());
-
- titleName = npdm.TitleName;
- titleId = npdm.Aci0.TitleId.ToString("x16");
- }
- }
- else
- {
- GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out titleId);
-
- // Check if there is an update available.
- if (IsUpdateApplied(titleId, out IFileSystem updatedControlFs))
- {
- // Replace the original ControlFs by the updated one.
- controlFs = updatedControlFs;
- }
-
- ReadControlData(controlFs, controlHolder.ByteSpan);
-
- GetGameInformation(ref controlHolder.Value, out titleName, out _, out developer, out version);
-
- // Read the icon from the ControlFS and store it as a byte array
- try
- {
- using UniqueRef icon = new();
-
- controlFs.OpenFile(ref icon.Ref, $"/icon_{_desiredTitleLanguage}.dat".ToU8Span(), OpenMode.Read).ThrowIfFailure();
-
- using MemoryStream stream = new();
-
- icon.Get.AsStream().CopyTo(stream);
- applicationIcon = stream.ToArray();
- }
- catch (HorizonResultException)
- {
- foreach (DirectoryEntryEx entry in controlFs.EnumerateEntries("/", "*"))
- {
- if (entry.Name == "control.nacp")
- {
- continue;
- }
-
- using var icon = new UniqueRef();
-
- controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
-
- using MemoryStream stream = new();
-
- icon.Get.AsStream().CopyTo(stream);
- applicationIcon = stream.ToArray();
-
- if (applicationIcon != null)
- {
- break;
- }
- }
-
- applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon;
- }
- }
- }
- catch (MissingKeyException exception)
- {
- applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon;
-
- Logger.Warning?.Print(LogClass.Application, $"Your key set is missing a key with the name: {exception.Name}");
- }
- catch (InvalidDataException)
- {
- applicationIcon = extension == ".xci" ? _xciIcon : _nspIcon;
-
- Logger.Warning?.Print(LogClass.Application, $"The header key is incorrect or missing and therefore the NCA header content type check has failed. Errored File: {applicationPath}");
- }
- catch (Exception exception)
- {
- Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. File: '{applicationPath}' Error: {exception}");
-
- numApplicationsFound--;
-
- continue;
- }
+ AppData = application,
+ });
}
- else if (extension == ".nro")
+
+ if (applications.Count > 1)
{
- BinaryReader reader = new(file);
-
- byte[] Read(long position, int size)
- {
- file.Seek(position, SeekOrigin.Begin);
-
- return reader.ReadBytes(size);
- }
-
- try
- {
- file.Seek(24, SeekOrigin.Begin);
-
- int assetOffset = reader.ReadInt32();
-
- if (Encoding.ASCII.GetString(Read(assetOffset, 4)) == "ASET")
- {
- byte[] iconSectionInfo = Read(assetOffset + 8, 0x10);
-
- long iconOffset = BitConverter.ToInt64(iconSectionInfo, 0);
- long iconSize = BitConverter.ToInt64(iconSectionInfo, 8);
-
- ulong nacpOffset = reader.ReadUInt64();
- ulong nacpSize = reader.ReadUInt64();
-
- // Reads and stores game icon as byte array
- if (iconSize > 0)
- {
- applicationIcon = Read(assetOffset + iconOffset, (int)iconSize);
- }
- else
- {
- applicationIcon = _nroIcon;
- }
-
- // Read the NACP data
- Read(assetOffset + (int)nacpOffset, (int)nacpSize).AsSpan().CopyTo(controlHolder.ByteSpan);
-
- GetGameInformation(ref controlHolder.Value, out titleName, out titleId, out developer, out version);
- }
- else
- {
- applicationIcon = _nroIcon;
- titleName = Path.GetFileNameWithoutExtension(applicationPath);
- }
- }
- catch
- {
- Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}");
-
- numApplicationsFound--;
-
- continue;
- }
+ numApplicationsFound += applications.Count - 1;
}
- else if (extension == ".nca")
- {
- try
- {
- Nca nca = new(_virtualFileSystem.KeySet, new FileStream(applicationPath, FileMode.Open, FileAccess.Read).AsStorage());
- int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
- if (nca.Header.ContentType != NcaContentType.Program || (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection()))
- {
- numApplicationsFound--;
-
- continue;
- }
- }
- catch (InvalidDataException)
- {
- Logger.Warning?.Print(LogClass.Application, $"The NCA header content type check has failed. This is usually because the header key is incorrect or missing. Errored File: {applicationPath}");
- }
- catch
- {
- Logger.Warning?.Print(LogClass.Application, $"The file encountered was not of a valid type. Errored File: {applicationPath}");
-
- numApplicationsFound--;
-
- continue;
- }
-
- applicationIcon = _ncaIcon;
- titleName = Path.GetFileNameWithoutExtension(applicationPath);
- }
- // If its an NSO we just set defaults
- else if (extension == ".nso")
- {
- applicationIcon = _nsoIcon;
- titleName = Path.GetFileNameWithoutExtension(applicationPath);
- }
+ numApplicationsLoaded += applications.Count;
}
- catch (IOException exception)
+ else
{
- Logger.Warning?.Print(LogClass.Application, exception.Message);
-
numApplicationsFound--;
-
- continue;
}
- ApplicationMetadata appMetadata = LoadAndSaveMetaData(titleId, appMetadata =>
- {
- appMetadata.Title = titleName;
-
- // Only do the migration if time_played has a value and timespan_played hasn't been updated yet.
- if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero)
- {
- appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld);
- appMetadata.TimePlayedOld = default;
- }
-
- // Only do the migration if last_played has a value and last_played_utc doesn't exist yet.
- if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue)
- {
- // Migrate from string-based last_played to DateTime-based last_played_utc.
- if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed))
- {
- appMetadata.LastPlayed = lastPlayedOldParsed;
-
- // Migration successful: deleting last_played from the metadata file.
- appMetadata.LastPlayedOld = default;
- }
-
- }
- });
-
- ApplicationData data = new()
- {
- Favorite = appMetadata.Favorite,
- Icon = applicationIcon,
- TitleName = titleName,
- TitleId = titleId,
- Developer = developer,
- Version = version,
- TimePlayed = appMetadata.TimePlayed,
- LastPlayed = appMetadata.LastPlayed,
- FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(),
- FileSize = fileSize,
- Path = applicationPath,
- ControlHolder = controlHolder,
- };
-
- numApplicationsLoaded++;
-
- OnApplicationAdded(new ApplicationAddedEventArgs
- {
- AppData = data,
- });
-
OnApplicationCountUpdated(new ApplicationCountUpdatedEventArgs
{
NumAppsFound = numApplicationsFound,
@@ -508,15 +604,6 @@ namespace Ryujinx.UI.App.Common
ApplicationCountUpdated?.Invoke(null, e);
}
- private void GetControlFsAndTitleId(IFileSystem pfs, out IFileSystem controlFs, out string titleId)
- {
- (_, _, Nca controlNca) = GetGameData(_virtualFileSystem, pfs, 0);
-
- // Return the ControlFS
- controlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
- titleId = controlNca?.Header.TitleId.ToString("x16");
- }
-
public static ApplicationMetadata LoadAndSaveMetaData(string titleId, Action modifyFunction = null)
{
string metadataFolder = Path.Combine(AppDataManager.GamesDirPath, titleId, "gui");
@@ -554,10 +641,29 @@ namespace Ryujinx.UI.App.Common
return appMetadata;
}
- public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage)
+ public byte[] GetApplicationIcon(string applicationPath, Language desiredTitleLanguage, ulong applicationId)
{
byte[] applicationIcon = null;
+ if (applicationId == 0)
+ {
+ if (Directory.Exists(applicationPath))
+ {
+ return _ncaIcon;
+ }
+
+ return Path.GetExtension(applicationPath).ToLower() switch
+ {
+ ".nsp" => _nspIcon,
+ ".pfs0" => _nspIcon,
+ ".xci" => _xciIcon,
+ ".nso" => _nsoIcon,
+ ".nro" => _nroIcon,
+ ".nca" => _ncaIcon,
+ _ => _ncaIcon,
+ };
+ }
+
try
{
// Look for icon only if applicationPath is not a directory
@@ -603,7 +709,16 @@ namespace Ryujinx.UI.App.Common
else
{
// Store the ControlFS in variable called controlFs
- GetControlFsAndTitleId(pfs, out IFileSystem controlFs, out _);
+ Dictionary programs = pfs.GetContentData(ContentMetaType.Application, _virtualFileSystem, _checkLevel);
+ IFileSystem controlFs = null;
+
+ if (programs.TryGetValue(applicationId, out ContentMetaData value))
+ {
+ if (value.GetNcaByType(_virtualFileSystem.KeySet, ContentType.Control) is { } controlNca)
+ {
+ controlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
+ }
+ }
// Read the icon from the ControlFS and store it as a byte array
try
@@ -630,16 +745,11 @@ namespace Ryujinx.UI.App.Common
controlFs.OpenFile(ref icon.Ref, entry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
- using (MemoryStream stream = new())
- {
- icon.Get.AsStream().CopyTo(stream);
- applicationIcon = stream.ToArray();
- }
+ using MemoryStream stream = new();
+ icon.Get.AsStream().CopyTo(stream);
+ applicationIcon = stream.ToArray();
- if (applicationIcon != null)
- {
- break;
- }
+ break;
}
applicationIcon ??= extension == ".xci" ? _xciIcon : _nspIcon;
@@ -722,80 +832,79 @@ namespace Ryujinx.UI.App.Common
return applicationIcon ?? _ncaIcon;
}
- private void GetGameInformation(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher, out string version)
+ private void GetApplicationInformation(ref ApplicationControlProperty controlData, ref ApplicationData data)
{
_ = Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage);
if (controlData.Title.ItemsRo.Length > (int)desiredTitleLanguage)
{
- titleName = controlData.Title[(int)desiredTitleLanguage].NameString.ToString();
- publisher = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString();
+ data.Name = controlData.Title[(int)desiredTitleLanguage].NameString.ToString();
+ data.Developer = controlData.Title[(int)desiredTitleLanguage].PublisherString.ToString();
}
else
{
- titleName = null;
- publisher = null;
+ data.Name = null;
+ data.Developer = null;
}
- if (string.IsNullOrWhiteSpace(titleName))
+ if (string.IsNullOrWhiteSpace(data.Name))
{
foreach (ref readonly var controlTitle in controlData.Title.ItemsRo)
{
if (!controlTitle.NameString.IsEmpty())
{
- titleName = controlTitle.NameString.ToString();
+ data.Name = controlTitle.NameString.ToString();
break;
}
}
}
- if (string.IsNullOrWhiteSpace(publisher))
+ if (string.IsNullOrWhiteSpace(data.Developer))
{
foreach (ref readonly var controlTitle in controlData.Title.ItemsRo)
{
if (!controlTitle.PublisherString.IsEmpty())
{
- publisher = controlTitle.PublisherString.ToString();
+ data.Developer = controlTitle.PublisherString.ToString();
break;
}
}
}
- if (controlData.PresenceGroupId != 0)
+ if (data.Id == 0)
{
- titleId = controlData.PresenceGroupId.ToString("x16");
- }
- else if (controlData.SaveDataOwnerId != 0)
- {
- titleId = controlData.SaveDataOwnerId.ToString();
- }
- else if (controlData.AddOnContentBaseId != 0)
- {
- titleId = (controlData.AddOnContentBaseId - 0x1000).ToString("x16");
- }
- else
- {
- titleId = "0000000000000000";
+ if (controlData.SaveDataOwnerId != 0)
+ {
+ data.Id = controlData.SaveDataOwnerId;
+ }
+ else if (controlData.PresenceGroupId != 0)
+ {
+ data.Id = controlData.PresenceGroupId;
+ }
+ else if (controlData.AddOnContentBaseId != 0)
+ {
+ data.Id = (controlData.AddOnContentBaseId - 0x1000);
+ }
}
- version = controlData.DisplayVersionString.ToString();
+ data.Version = controlData.DisplayVersionString.ToString();
}
- private bool IsUpdateApplied(string titleId, out IFileSystem updatedControlFs)
+ private bool IsUpdateApplied(Nca mainNca, out IFileSystem updatedControlFs)
{
updatedControlFs = null;
- string updatePath = "(unknown)";
+ string updatePath = null;
try
{
- (Nca patchNca, Nca controlNca) = GetGameUpdateData(_virtualFileSystem, titleId, 0, out updatePath);
+ (Nca patchNca, Nca controlNca) = mainNca.GetUpdateData(_virtualFileSystem, _checkLevel, 0, out updatePath);
if (patchNca != null && controlNca != null)
{
- updatedControlFs = controlNca?.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
+ updatedControlFs = controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None);
return true;
}
@@ -811,120 +920,5 @@ namespace Ryujinx.UI.App.Common
return false;
}
-
- public static (Nca main, Nca patch, Nca control) GetGameData(VirtualFileSystem fileSystem, IFileSystem pfs, int programIndex)
- {
- Nca mainNca = null;
- Nca patchNca = null;
- Nca controlNca = null;
-
- fileSystem.ImportTickets(pfs);
-
- foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
- {
- using var ncaFile = new UniqueRef();
-
- pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
-
- Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage());
-
- int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF);
-
- if (ncaProgramIndex != programIndex)
- {
- continue;
- }
-
- if (nca.Header.ContentType == NcaContentType.Program)
- {
- int dataIndex = Nca.GetSectionIndexFromType(NcaSectionType.Data, NcaContentType.Program);
-
- if (nca.SectionExists(NcaSectionType.Data) && nca.Header.GetFsHeader(dataIndex).IsPatchSection())
- {
- patchNca = nca;
- }
- else
- {
- mainNca = nca;
- }
- }
- else if (nca.Header.ContentType == NcaContentType.Control)
- {
- controlNca = nca;
- }
- }
-
- return (mainNca, patchNca, controlNca);
- }
-
- public static (Nca patch, Nca control) GetGameUpdateDataFromPartition(VirtualFileSystem fileSystem, PartitionFileSystem pfs, string titleId, int programIndex)
- {
- Nca patchNca = null;
- Nca controlNca = null;
-
- fileSystem.ImportTickets(pfs);
-
- foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
- {
- using var ncaFile = new UniqueRef();
-
- pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
-
- Nca nca = new(fileSystem.KeySet, ncaFile.Release().AsStorage());
-
- int ncaProgramIndex = (int)(nca.Header.TitleId & 0xF);
-
- if (ncaProgramIndex != programIndex)
- {
- continue;
- }
-
- if ($"{nca.Header.TitleId.ToString("x16")[..^3]}000" != titleId)
- {
- break;
- }
-
- if (nca.Header.ContentType == NcaContentType.Program)
- {
- patchNca = nca;
- }
- else if (nca.Header.ContentType == NcaContentType.Control)
- {
- controlNca = nca;
- }
- }
-
- return (patchNca, controlNca);
- }
-
- public static (Nca patch, Nca control) GetGameUpdateData(VirtualFileSystem fileSystem, string titleId, int programIndex, out string updatePath)
- {
- updatePath = null;
-
- if (ulong.TryParse(titleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdBase))
- {
- // Clear the program index part.
- titleIdBase &= ~0xFUL;
-
- // Load update information if exists.
- string titleUpdateMetadataPath = Path.Combine(AppDataManager.GamesDirPath, titleIdBase.ToString("x16"), "updates.json");
-
- if (File.Exists(titleUpdateMetadataPath))
- {
- updatePath = JsonHelper.DeserializeFromFile(titleUpdateMetadataPath, _titleSerializerContext.TitleUpdateMetadata).Selected;
-
- if (File.Exists(updatePath))
- {
- FileStream file = new(updatePath, FileMode.Open, FileAccess.Read);
- PartitionFileSystem nsp = new();
- nsp.Initialize(file.AsStorage()).ThrowIfFailure();
-
- return GetGameUpdateDataFromPartition(fileSystem, nsp, titleIdBase.ToString("x16"), programIndex);
- }
- }
- }
-
- return (null, null);
- }
}
}
diff --git a/src/Ryujinx.UI.Common/Helper/CommandLineState.cs b/src/Ryujinx.UI.Common/Helper/CommandLineState.cs
index bbacd5fec..ae0e4d904 100644
--- a/src/Ryujinx.UI.Common/Helper/CommandLineState.cs
+++ b/src/Ryujinx.UI.Common/Helper/CommandLineState.cs
@@ -14,6 +14,7 @@ namespace Ryujinx.UI.Common.Helper
public static string BaseDirPathArg { get; private set; }
public static string Profile { get; private set; }
public static string LaunchPathArg { get; private set; }
+ public static string LaunchApplicationId { get; private set; }
public static bool StartFullscreenArg { get; private set; }
public static void ParseArguments(string[] args)
@@ -72,6 +73,10 @@ namespace Ryujinx.UI.Common.Helper
OverrideGraphicsBackend = args[++i];
break;
+ case "-i":
+ case "--application-id":
+ LaunchApplicationId = args[++i];
+ break;
case "--docked-mode":
OverrideDockedMode = true;
break;
diff --git a/src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs b/src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs
index c2085b28c..58bdc90e6 100644
--- a/src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs
+++ b/src/Ryujinx.UI.Common/Helper/ShortcutHelper.cs
@@ -15,7 +15,7 @@ namespace Ryujinx.UI.Common.Helper
public static class ShortcutHelper
{
[SupportedOSPlatform("windows")]
- private static void CreateShortcutWindows(string applicationFilePath, byte[] iconData, string iconPath, string cleanedAppName, string desktopPath)
+ private static void CreateShortcutWindows(string applicationFilePath, string applicationId, byte[] iconData, string iconPath, string cleanedAppName, string desktopPath)
{
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, AppDomain.CurrentDomain.FriendlyName + ".exe");
iconPath += ".ico";
@@ -25,13 +25,13 @@ namespace Ryujinx.UI.Common.Helper
image.Mutate(x => x.Resize(128, 128));
SaveBitmapAsIcon(image, iconPath);
- var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(applicationFilePath), iconPath, 0);
+ var shortcut = Shortcut.CreateShortcut(basePath, GetArgsString(applicationFilePath, applicationId), iconPath, 0);
shortcut.StringData.NameString = cleanedAppName;
shortcut.WriteToFile(Path.Combine(desktopPath, cleanedAppName + ".lnk"));
}
[SupportedOSPlatform("linux")]
- private static void CreateShortcutLinux(string applicationFilePath, byte[] iconData, string iconPath, string desktopPath, string cleanedAppName)
+ private static void CreateShortcutLinux(string applicationFilePath, string applicationId, byte[] iconData, string iconPath, string desktopPath, string cleanedAppName)
{
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx.sh");
var desktopFile = EmbeddedResources.ReadAllText("Ryujinx.UI.Common/shortcut-template.desktop");
@@ -41,11 +41,11 @@ namespace Ryujinx.UI.Common.Helper
image.SaveAsPng(iconPath);
using StreamWriter outputFile = new(Path.Combine(desktopPath, cleanedAppName + ".desktop"));
- outputFile.Write(desktopFile, cleanedAppName, iconPath, $"{basePath} {GetArgsString(applicationFilePath)}");
+ outputFile.Write(desktopFile, cleanedAppName, iconPath, $"{basePath} {GetArgsString(applicationFilePath, applicationId)}");
}
[SupportedOSPlatform("macos")]
- private static void CreateShortcutMacos(string appFilePath, byte[] iconData, string desktopPath, string cleanedAppName)
+ private static void CreateShortcutMacos(string appFilePath, string applicationId, byte[] iconData, string desktopPath, string cleanedAppName)
{
string basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Ryujinx");
var plistFile = EmbeddedResources.ReadAllText("Ryujinx.UI.Common/shortcut-template.plist");
@@ -64,7 +64,7 @@ namespace Ryujinx.UI.Common.Helper
string scriptPath = Path.Combine(scriptFolderPath, ScriptName);
using StreamWriter scriptFile = new(scriptPath);
- scriptFile.Write(shortcutScript, basePath, GetArgsString(appFilePath));
+ scriptFile.Write(shortcutScript, basePath, GetArgsString(appFilePath, applicationId));
// Set execute permission
FileInfo fileInfo = new(scriptPath);
@@ -95,7 +95,7 @@ namespace Ryujinx.UI.Common.Helper
{
string iconPath = Path.Combine(AppDataManager.BaseDirPath, "games", applicationId, "app");
- CreateShortcutWindows(applicationFilePath, iconData, iconPath, cleanedAppName, desktopPath);
+ CreateShortcutWindows(applicationFilePath, applicationId, iconData, iconPath, cleanedAppName, desktopPath);
return;
}
@@ -105,14 +105,14 @@ namespace Ryujinx.UI.Common.Helper
string iconPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local", "share", "icons", "Ryujinx");
Directory.CreateDirectory(iconPath);
- CreateShortcutLinux(applicationFilePath, iconData, Path.Combine(iconPath, applicationId), desktopPath, cleanedAppName);
+ CreateShortcutLinux(applicationFilePath, applicationId, iconData, Path.Combine(iconPath, applicationId), desktopPath, cleanedAppName);
return;
}
if (OperatingSystem.IsMacOS())
{
- CreateShortcutMacos(applicationFilePath, iconData, desktopPath, cleanedAppName);
+ CreateShortcutMacos(applicationFilePath, applicationId, iconData, desktopPath, cleanedAppName);
return;
}
@@ -120,7 +120,7 @@ namespace Ryujinx.UI.Common.Helper
throw new NotImplementedException("Shortcut support has not been implemented yet for this OS.");
}
- private static string GetArgsString(string appFilePath)
+ private static string GetArgsString(string appFilePath, string applicationId)
{
// args are first defined as a list, for easier adjustments in the future
var argsList = new List();
@@ -131,6 +131,12 @@ namespace Ryujinx.UI.Common.Helper
argsList.Add($"\"{CommandLineState.BaseDirPathArg}\"");
}
+ if (appFilePath.ToLower().EndsWith(".xci"))
+ {
+ argsList.Add("--application-id");
+ argsList.Add($"\"{applicationId}\"");
+ }
+
argsList.Add($"\"{appFilePath}\"");
return String.Join(" ", argsList);
diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs
index 7004908a7..8c643f340 100644
--- a/src/Ryujinx/AppHost.cs
+++ b/src/Ryujinx/AppHost.cs
@@ -132,12 +132,14 @@ namespace Ryujinx.Ava
public int Width { get; private set; }
public int Height { get; private set; }
public string ApplicationPath { get; private set; }
+ public ulong ApplicationId { get; private set; }
public bool ScreenshotRequested { get; set; }
public AppHost(
RendererHost renderer,
InputManager inputManager,
string applicationPath,
+ ulong applicationId,
VirtualFileSystem virtualFileSystem,
ContentManager contentManager,
AccountManager accountManager,
@@ -161,6 +163,7 @@ namespace Ryujinx.Ava
NpadManager = _inputManager.CreateNpadManager();
TouchScreenManager = _inputManager.CreateTouchScreenManager();
ApplicationPath = applicationPath;
+ ApplicationId = applicationId;
VirtualFileSystem = virtualFileSystem;
ContentManager = contentManager;
@@ -719,7 +722,7 @@ namespace Ryujinx.Ava
{
Logger.Info?.Print(LogClass.Application, "Loading as XCI.");
- if (!Device.LoadXci(ApplicationPath))
+ if (!Device.LoadXci(ApplicationPath, ApplicationId))
{
Device.Dispose();
@@ -746,7 +749,7 @@ namespace Ryujinx.Ava
{
Logger.Info?.Print(LogClass.Application, "Loading as NSP.");
- if (!Device.LoadNsp(ApplicationPath))
+ if (!Device.LoadNsp(ApplicationPath, ApplicationId))
{
Device.Dispose();
diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json
index 8df0f96a1..74e18056b 100644
--- a/src/Ryujinx/Assets/Locales/en_US.json
+++ b/src/Ryujinx/Assets/Locales/en_US.json
@@ -10,6 +10,7 @@
"SettingsTabSystemUseHypervisor": "Use Hypervisor",
"MenuBarFile": "_File",
"MenuBarFileOpenFromFile": "_Load Application From File",
+ "MenuBarFileOpenFromFileError": "No applications found in selected file.",
"MenuBarFileOpenUnpacked": "Load _Unpacked Game",
"MenuBarFileOpenEmuFolder": "Open Ryujinx Folder",
"MenuBarFileOpenLogsFolder": "Open Logs Folder",
@@ -649,6 +650,8 @@
"OpenSetupGuideMessage": "Open the Setup Guide",
"NoUpdate": "No Update",
"TitleUpdateVersionLabel": "Version {0}",
+ "TitleBundledUpdateVersionLabel": "Bundled: Version {0}",
+ "TitleBundledDlcLabel": "Bundled:",
"RyujinxInfo": "Ryujinx - Info",
"RyujinxConfirm": "Ryujinx - Confirmation",
"FileDialogAllTypes": "All types",
diff --git a/src/Ryujinx/Common/ApplicationHelper.cs b/src/Ryujinx/Common/ApplicationHelper.cs
index 622a6a024..14773114c 100644
--- a/src/Ryujinx/Common/ApplicationHelper.cs
+++ b/src/Ryujinx/Common/ApplicationHelper.cs
@@ -18,7 +18,8 @@ using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.FileSystem;
using Ryujinx.HLE.HOS.Services.Account.Acc;
-using Ryujinx.UI.App.Common;
+using Ryujinx.HLE.Loaders.Processes.Extensions;
+using Ryujinx.UI.Common.Configuration;
using Ryujinx.UI.Common.Helper;
using System;
using System.Buffers;
@@ -226,7 +227,11 @@ namespace Ryujinx.Ava.Common
return;
}
- (Nca updatePatchNca, _) = ApplicationLibrary.GetGameUpdateData(_virtualFileSystem, mainNca.Header.TitleId.ToString("x16"), programIndex, out _);
+ IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
+ ? IntegrityCheckLevel.ErrorOnInvalid
+ : IntegrityCheckLevel.None;
+
+ (Nca updatePatchNca, _) = mainNca.GetUpdateData(_virtualFileSystem, checkLevel, programIndex, out _);
if (updatePatchNca != null)
{
patchNca = updatePatchNca;
diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs
index 4f68ca24f..976963422 100644
--- a/src/Ryujinx/Program.cs
+++ b/src/Ryujinx/Program.cs
@@ -125,7 +125,7 @@ namespace Ryujinx.Ava
if (CommandLineState.LaunchPathArg != null)
{
- MainWindow.DeferLoadApplication(CommandLineState.LaunchPathArg, CommandLineState.StartFullscreenArg);
+ MainWindow.DeferLoadApplication(CommandLineState.LaunchPathArg, CommandLineState.LaunchApplicationId, CommandLineState.StartFullscreenArg);
}
}
diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs
index 894ac6c1a..5edd02308 100644
--- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs
+++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml.cs
@@ -1,7 +1,6 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
-using Avalonia.Threading;
using LibHac.Fs;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common;
@@ -15,7 +14,6 @@ using Ryujinx.UI.App.Common;
using Ryujinx.UI.Common.Helper;
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.IO;
using Path = System.IO.Path;
@@ -41,7 +39,7 @@ namespace Ryujinx.Ava.UI.Controls
{
viewModel.SelectedApplication.Favorite = !viewModel.SelectedApplication.Favorite;
- ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.TitleId, appMetadata =>
+ ApplicationLibrary.LoadAndSaveMetaData(viewModel.SelectedApplication.IdString, appMetadata =>
{
appMetadata.Favorite = viewModel.SelectedApplication.Favorite;
});
@@ -76,19 +74,9 @@ namespace Ryujinx.Ava.UI.Controls
{
if (viewModel?.SelectedApplication != null)
{
- if (!ulong.TryParse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out ulong titleIdNumber))
- {
- Dispatcher.UIThread.InvokeAsync(async () =>
- {
- await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogRyujinxErrorMessage], LocaleManager.Instance[LocaleKeys.DialogInvalidTitleIdErrorMessage]);
- });
+ var saveDataFilter = SaveDataFilter.Make(viewModel.SelectedApplication.Id, saveDataType, userId, saveDataId: default, index: default);
- return;
- }
-
- var saveDataFilter = SaveDataFilter.Make(titleIdNumber, saveDataType, userId, saveDataId: default, index: default);
-
- ApplicationHelper.OpenSaveDir(in saveDataFilter, titleIdNumber, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.TitleName);
+ ApplicationHelper.OpenSaveDir(in saveDataFilter, viewModel.SelectedApplication.Id, viewModel.SelectedApplication.ControlHolder, viewModel.SelectedApplication.Name);
}
}
@@ -98,7 +86,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null)
{
- await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName);
+ await TitleUpdateWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication);
}
}
@@ -108,7 +96,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null)
{
- await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName);
+ await DownloadableContentManagerWindow.Show(viewModel.VirtualFileSystem, viewModel.SelectedApplication);
}
}
@@ -120,8 +108,8 @@ namespace Ryujinx.Ava.UI.Controls
{
await new CheatWindow(
viewModel.VirtualFileSystem,
- viewModel.SelectedApplication.TitleId,
- viewModel.SelectedApplication.TitleName,
+ viewModel.SelectedApplication.IdString,
+ viewModel.SelectedApplication.Name,
viewModel.SelectedApplication.Path).ShowDialog(viewModel.TopLevel as Window);
}
}
@@ -133,7 +121,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null)
{
string modsBasePath = ModLoader.GetModsBasePath();
- string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, viewModel.SelectedApplication.TitleId);
+ string titleModsPath = ModLoader.GetApplicationDir(modsBasePath, viewModel.SelectedApplication.IdString);
OpenHelper.OpenFolder(titleModsPath);
}
@@ -146,7 +134,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null)
{
string sdModsBasePath = ModLoader.GetSdModsBasePath();
- string titleModsPath = ModLoader.GetApplicationDir(sdModsBasePath, viewModel.SelectedApplication.TitleId);
+ string titleModsPath = ModLoader.GetApplicationDir(sdModsBasePath, viewModel.SelectedApplication.IdString);
OpenHelper.OpenFolder(titleModsPath);
}
@@ -158,7 +146,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null)
{
- await ModManagerWindow.Show(ulong.Parse(viewModel.SelectedApplication.TitleId, NumberStyles.HexNumber), viewModel.SelectedApplication.TitleName);
+ await ModManagerWindow.Show(viewModel.SelectedApplication.Id, viewModel.SelectedApplication.Name);
}
}
@@ -170,15 +158,15 @@ namespace Ryujinx.Ava.UI.Controls
{
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
LocaleManager.Instance[LocaleKeys.DialogWarning],
- LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.TitleName),
+ LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogPPTCDeletionMessage, viewModel.SelectedApplication.Name),
LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
if (result == UserResult.Yes)
{
- DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "0"));
- DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu", "1"));
+ DirectoryInfo mainDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "0"));
+ DirectoryInfo backupDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu", "1"));
List cacheFiles = new();
@@ -218,14 +206,14 @@ namespace Ryujinx.Ava.UI.Controls
{
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
LocaleManager.Instance[LocaleKeys.DialogWarning],
- LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.TitleName),
+ LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogShaderDeletionMessage, viewModel.SelectedApplication.Name),
LocaleManager.Instance[LocaleKeys.InputDialogYes],
LocaleManager.Instance[LocaleKeys.InputDialogNo],
LocaleManager.Instance[LocaleKeys.RyujinxConfirm]);
if (result == UserResult.Yes)
{
- DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader"));
+ DirectoryInfo shaderCacheDir = new(Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader"));
List oldCacheDirectories = new();
List newCacheFiles = new();
@@ -273,7 +261,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null)
{
- string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "cpu");
+ string ptcDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "cpu");
string mainDir = Path.Combine(ptcDir, "0");
string backupDir = Path.Combine(ptcDir, "1");
@@ -294,7 +282,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null)
{
- string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.TitleId, "cache", "shader");
+ string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader");
if (!Directory.Exists(shaderCacheDir))
{
@@ -315,7 +303,7 @@ namespace Ryujinx.Ava.UI.Controls
viewModel.StorageProvider,
NcaSectionType.Code,
viewModel.SelectedApplication.Path,
- viewModel.SelectedApplication.TitleName);
+ viewModel.SelectedApplication.Name);
}
}
@@ -329,7 +317,7 @@ namespace Ryujinx.Ava.UI.Controls
viewModel.StorageProvider,
NcaSectionType.Data,
viewModel.SelectedApplication.Path,
- viewModel.SelectedApplication.TitleName);
+ viewModel.SelectedApplication.Name);
}
}
@@ -343,7 +331,7 @@ namespace Ryujinx.Ava.UI.Controls
viewModel.StorageProvider,
NcaSectionType.Logo,
viewModel.SelectedApplication.Path,
- viewModel.SelectedApplication.TitleName);
+ viewModel.SelectedApplication.Name);
}
}
@@ -354,7 +342,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null)
{
ApplicationData selectedApplication = viewModel.SelectedApplication;
- ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.TitleName, selectedApplication.TitleId, selectedApplication.Icon);
+ ShortcutHelper.CreateAppShortcut(selectedApplication.Path, selectedApplication.Name, selectedApplication.IdString, selectedApplication.Icon);
}
}
@@ -364,7 +352,7 @@ namespace Ryujinx.Ava.UI.Controls
if (viewModel?.SelectedApplication != null)
{
- await viewModel.LoadApplication(viewModel.SelectedApplication.Path);
+ await viewModel.LoadApplication(viewModel.SelectedApplication);
}
}
}
diff --git a/src/Ryujinx/UI/Controls/ApplicationGridView.axaml b/src/Ryujinx/UI/Controls/ApplicationGridView.axaml
index 2dc95662a..98a1c004b 100644
--- a/src/Ryujinx/UI/Controls/ApplicationGridView.axaml
+++ b/src/Ryujinx/UI/Controls/ApplicationGridView.axaml
@@ -80,7 +80,7 @@
diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml b/src/Ryujinx/UI/Controls/ApplicationListView.axaml
index fecf08883..f99cf316e 100644
--- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml
+++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml
@@ -85,7 +85,7 @@
Path.GetFileName(ContainerPath);
+ public string Label =>
+ Path.GetExtension(FileName)?.ToLower() == ".xci" ? $"{LocaleManager.Instance[LocaleKeys.TitleBundledDlcLabel]} {FileName}" : FileName;
+
public DownloadableContentModel(string titleId, string containerPath, string fullPath, bool enabled)
{
TitleId = titleId;
diff --git a/src/Ryujinx/UI/Models/SaveModel.cs b/src/Ryujinx/UI/Models/SaveModel.cs
index d6dea2f69..181295b06 100644
--- a/src/Ryujinx/UI/Models/SaveModel.cs
+++ b/src/Ryujinx/UI/Models/SaveModel.cs
@@ -46,14 +46,14 @@ namespace Ryujinx.Ava.UI.Models
TitleId = info.ProgramId;
UserId = info.UserId;
- var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.TitleId.ToUpper() == TitleIdString);
+ var appData = MainWindow.MainWindowViewModel.Applications.FirstOrDefault(x => x.IdString.ToUpper() == TitleIdString);
InGameList = appData != null;
if (InGameList)
{
Icon = appData.Icon;
- Title = appData.TitleName;
+ Title = appData.Name;
}
else
{
diff --git a/src/Ryujinx/UI/Models/TitleUpdateModel.cs b/src/Ryujinx/UI/Models/TitleUpdateModel.cs
index c270c9ed4..cde37bf91 100644
--- a/src/Ryujinx/UI/Models/TitleUpdateModel.cs
+++ b/src/Ryujinx/UI/Models/TitleUpdateModel.cs
@@ -8,7 +8,10 @@ namespace Ryujinx.Ava.UI.Models
public ApplicationControlProperty Control { get; }
public string Path { get; }
- public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleUpdateVersionLabel, Control.DisplayVersionString.ToString());
+ public string Label => LocaleManager.Instance.UpdateAndGetDynamicValue(
+ System.IO.Path.GetExtension(Path)?.ToLower() == ".xci" ? LocaleKeys.TitleBundledUpdateVersionLabel : LocaleKeys.TitleUpdateVersionLabel,
+ Control.DisplayVersionString.ToString()
+ );
public TitleUpdateModel(ApplicationControlProperty control, string path)
{
diff --git a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs
index 2cd714f44..0f500513a 100644
--- a/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/DownloadableContentManagerViewModel.cs
@@ -6,7 +6,6 @@ using DynamicData;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
-using LibHac.FsSystem;
using LibHac.Tools.Fs;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
@@ -17,11 +16,13 @@ using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.Loaders.Processes.Extensions;
+using Ryujinx.HLE.Utilities;
+using Ryujinx.UI.App.Common;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Threading.Tasks;
using Application = Avalonia.Application;
using Path = System.IO.Path;
@@ -38,7 +39,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private AvaloniaList _selectedDownloadableContents = new();
private string _search;
- private readonly ulong _titleId;
+ private readonly ApplicationData _applicationData;
private readonly IStorageProvider _storageProvider;
private static readonly DownloadableContentJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
@@ -91,18 +92,25 @@ namespace Ryujinx.Ava.UI.ViewModels
get => string.Format(LocaleManager.Instance[LocaleKeys.DlcWindowHeading], DownloadableContents.Count);
}
- public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ulong titleId)
+ public DownloadableContentManagerViewModel(VirtualFileSystem virtualFileSystem, ApplicationData applicationData)
{
_virtualFileSystem = virtualFileSystem;
- _titleId = titleId;
+ _applicationData = applicationData;
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
_storageProvider = desktop.MainWindow.StorageProvider;
}
- _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, titleId.ToString("x16"), "dlc.json");
+ _downloadableContentJsonPath = Path.Combine(AppDataManager.GamesDirPath, applicationData.IdString, "dlc.json");
+
+ if (!File.Exists(_downloadableContentJsonPath))
+ {
+ _downloadableContentContainerList = new List();
+
+ Save();
+ }
try
{
@@ -123,12 +131,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
if (File.Exists(downloadableContentContainer.ContainerPath))
{
- using FileStream containerFile = File.OpenRead(downloadableContentContainer.ContainerPath);
-
- PartitionFileSystem partitionFileSystem = new();
- partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
-
- _virtualFileSystem.ImportTickets(partitionFileSystem);
+ using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(downloadableContentContainer.ContainerPath, _virtualFileSystem);
foreach (DownloadableContentNca downloadableContentNca in downloadableContentContainer.DownloadableContentNcaList)
{
@@ -157,6 +160,9 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
+ // NOTE: Try to load downloadable contents from PFS last to preserve enabled state.
+ AddDownloadableContent(_applicationData.Path);
+
// NOTE: Save the list again to remove leftovers.
Save();
Sort();
@@ -219,25 +225,23 @@ namespace Ryujinx.Ava.UI.ViewModels
foreach (var file in result)
{
- await AddDownloadableContent(file.Path.LocalPath);
+ if (!AddDownloadableContent(file.Path.LocalPath))
+ {
+ await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
+ }
}
}
- private async Task AddDownloadableContent(string path)
+ private bool AddDownloadableContent(string path)
{
- if (!File.Exists(path) || DownloadableContents.FirstOrDefault(x => x.ContainerPath == path) != null)
+ if (!File.Exists(path) || _downloadableContentContainerList.Any(x => x.ContainerPath == path))
{
- return;
+ return true;
}
- using FileStream containerFile = File.OpenRead(path);
-
- PartitionFileSystem partitionFileSystem = new();
- partitionFileSystem.Initialize(containerFile.AsStorage()).ThrowIfFailure();
- bool containsDownloadableContent = false;
-
- _virtualFileSystem.ImportTickets(partitionFileSystem);
+ using IFileSystem partitionFileSystem = PartitionFileSystemUtils.OpenApplicationFileSystem(path, _virtualFileSystem);
+ bool success = false;
foreach (DirectoryEntryEx fileEntry in partitionFileSystem.EnumerateEntries("/", "*.nca"))
{
using var ncaFile = new UniqueRef();
@@ -252,26 +256,26 @@ namespace Ryujinx.Ava.UI.ViewModels
if (nca.Header.ContentType == NcaContentType.PublicData)
{
- if ((nca.Header.TitleId & 0xFFFFFFFFFFFFE000) != _titleId)
+ if (nca.GetProgramIdBase() != _applicationData.IdBase)
{
- break;
+ continue;
}
var content = new DownloadableContentModel(nca.Header.TitleId.ToString("X16"), path, fileEntry.FullPath, true);
DownloadableContents.Add(content);
SelectedDownloadableContents.Add(content);
- OnPropertyChanged(nameof(UpdateCount));
- Sort();
-
- containsDownloadableContent = true;
+ success = true;
}
}
- if (!containsDownloadableContent)
+ if (success)
{
- await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogDlcNoDlcErrorMessage]);
+ OnPropertyChanged(nameof(UpdateCount));
+ Sort();
}
+
+ return success;
}
public void Remove(DownloadableContentModel model)
diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs
index b47cc4b7c..134e90300 100644
--- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs
@@ -96,7 +96,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private bool _canUpdate = true;
private Cursor _cursor;
private string _title;
- private string _currentEmulatedGamePath;
+ private ApplicationData _currentApplicationData;
private readonly AutoResetEvent _rendererWaitEvent;
private WindowState _windowState;
private double _windowWidth;
@@ -108,7 +108,6 @@ namespace Ryujinx.Ava.UI.ViewModels
public ApplicationData ListSelectedApplication;
public ApplicationData GridSelectedApplication;
- private string TitleName { get; set; }
internal AppHost AppHost { get; set; }
public MainWindowViewModel()
@@ -954,8 +953,8 @@ namespace Ryujinx.Ava.UI.ViewModels
return SortMode switch
{
#pragma warning disable IDE0055 // Disable formatting
- ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.TitleName)
- : SortExpressionComparer.Descending(app => app.TitleName),
+ ApplicationSort.Title => IsAscending ? SortExpressionComparer.Ascending(app => app.Name)
+ : SortExpressionComparer.Descending(app => app.Name),
ApplicationSort.Developer => IsAscending ? SortExpressionComparer.Ascending(app => app.Developer)
: SortExpressionComparer.Descending(app => app.Developer),
ApplicationSort.LastPlayed => new LastPlayedSortComparer(IsAscending),
@@ -999,7 +998,7 @@ namespace Ryujinx.Ava.UI.ViewModels
CompareInfo compareInfo = CultureInfo.CurrentCulture.CompareInfo;
- return compareInfo.IndexOf(app.TitleName, _searchText, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) >= 0;
+ return compareInfo.IndexOf(app.Name, _searchText, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace) >= 0;
}
return false;
@@ -1128,7 +1127,7 @@ namespace Ryujinx.Ava.UI.ViewModels
IsLoadingIndeterminate = false;
break;
case LoadState.Loaded:
- LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName);
+ LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name);
IsLoadingIndeterminate = true;
CacheLoadStatus = "";
break;
@@ -1148,7 +1147,7 @@ namespace Ryujinx.Ava.UI.ViewModels
IsLoadingIndeterminate = false;
break;
case ShaderCacheLoadingState.Loaded:
- LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, TitleName);
+ LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, _currentApplicationData.Name);
IsLoadingIndeterminate = true;
CacheLoadStatus = "";
break;
@@ -1200,13 +1199,13 @@ namespace Ryujinx.Ava.UI.ViewModels
{
UserChannelPersistence.ShouldRestart = false;
- await LoadApplication(_currentEmulatedGamePath);
+ await LoadApplication(_currentApplicationData);
}
else
{
// Otherwise, clear state.
UserChannelPersistence = new UserChannelPersistence();
- _currentEmulatedGamePath = null;
+ _currentApplicationData = null;
}
}
@@ -1493,7 +1492,15 @@ namespace Ryujinx.Ava.UI.ViewModels
if (result.Count > 0)
{
- await LoadApplication(result[0].Path.LocalPath);
+ if (ApplicationLibrary.TryGetApplicationsFromFile(result[0].Path.LocalPath,
+ out List applications))
+ {
+ await LoadApplication(applications[0]);
+ }
+ else
+ {
+ await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.MenuBarFileOpenFromFileError]);
+ }
}
}
@@ -1507,11 +1514,17 @@ namespace Ryujinx.Ava.UI.ViewModels
if (result.Count > 0)
{
- await LoadApplication(result[0].Path.LocalPath);
+ ApplicationData applicationData = new()
+ {
+ Name = Path.GetFileNameWithoutExtension(result[0].Path.LocalPath),
+ Path = result[0].Path.LocalPath,
+ };
+
+ await LoadApplication(applicationData);
}
}
- public async Task LoadApplication(string path, bool startFullscreen = false, string titleName = "")
+ public async Task LoadApplication(ApplicationData application, bool startFullscreen = false)
{
if (AppHost != null)
{
@@ -1531,7 +1544,7 @@ namespace Ryujinx.Ava.UI.ViewModels
Logger.RestartTime();
- SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(path, ConfigurationState.Instance.System.Language);
+ SelectedIcon ??= ApplicationLibrary.GetApplicationIcon(application.Path, ConfigurationState.Instance.System.Language, application.Id);
PrepareLoadScreen();
@@ -1540,7 +1553,8 @@ namespace Ryujinx.Ava.UI.ViewModels
AppHost = new AppHost(
RendererHostControl,
InputManager,
- path,
+ application.Path,
+ application.Id,
VirtualFileSystem,
ContentManager,
AccountManager,
@@ -1558,17 +1572,17 @@ namespace Ryujinx.Ava.UI.ViewModels
CanUpdate = false;
- LoadHeading = TitleName = titleName;
+ LoadHeading = application.Name;
- if (string.IsNullOrWhiteSpace(titleName))
+ if (string.IsNullOrWhiteSpace(application.Name))
{
LoadHeading = LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.LoadingHeading, AppHost.Device.Processes.ActiveApplication.Name);
- TitleName = AppHost.Device.Processes.ActiveApplication.Name;
+ application.Name = AppHost.Device.Processes.ActiveApplication.Name;
}
SwitchToRenderer(startFullscreen);
- _currentEmulatedGamePath = path;
+ _currentApplicationData = application;
Thread gameThread = new(InitializeGame) { Name = "GUI.WindowThread" };
gameThread.Start();
diff --git a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs
index 5989ce09a..6c38edb37 100644
--- a/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs
+++ b/src/Ryujinx/UI/ViewModels/TitleUpdateViewModel.cs
@@ -1,4 +1,3 @@
-using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform.Storage;
@@ -6,7 +5,7 @@ using Avalonia.Threading;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
-using LibHac.FsSystem;
+using LibHac.Ncm;
using LibHac.Ns;
using LibHac.Tools.FsSystem;
using LibHac.Tools.FsSystem.NcaUtils;
@@ -17,12 +16,17 @@ using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.FileSystem;
+using Ryujinx.HLE.Loaders.Processes.Extensions;
+using Ryujinx.HLE.Utilities;
using Ryujinx.UI.App.Common;
+using Ryujinx.UI.Common.Configuration;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using Application = Avalonia.Application;
+using ContentType = LibHac.Ncm.ContentType;
using Path = System.IO.Path;
using SpanHelpers = LibHac.Common.SpanHelpers;
@@ -33,7 +37,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public TitleUpdateMetadata TitleUpdateWindowData;
public readonly string TitleUpdateJsonPath;
private VirtualFileSystem VirtualFileSystem { get; }
- private ulong TitleId { get; }
+ private ApplicationData ApplicationData { get; }
private AvaloniaList _titleUpdates = new();
private AvaloniaList