Compare commits

...

31 Commits

Author SHA1 Message Date
06abba25c1 UI: Adapt accent color to the user's system.
https://amwx.github.io/FluentAvaloniaDocs/pages/FATheme/Accents#using-the-systems-accent-color
2024-12-07 06:22:46 -06:00
de00a71690 UI: Fix missing total DLC count.
Fixes #347.
2024-12-07 05:48:11 -06:00
315a1819c0 Attempt #2 2024-12-07 05:31:37 -06:00
4ffb8aef12 Try and fix nullref 2024-12-07 05:21:16 -06:00
290a6ad5de HLE: extract custom NACP data functionality into a static helper for generic reuse elsewhere, and clarify magic numbers. 2024-12-07 04:30:04 -06:00
eda4f4349b headless: Actually log the command line errors 2024-12-07 04:06:22 -06:00
5fbcb1f3a7 misc: chore: Cleanups & unused parameter removal 2024-12-07 04:06:22 -06:00
d00754477e Add Firmware keyword in log if it is indeed firmware (#343)
Co-authored-by: LotP1 <rasmus.stilling.pedersen1@gmail.com>
2024-12-07 04:03:01 -06:00
0bc1eddaeb Update Spanish translation (#332)
- Added translations for XCI trimmer
- Added translations for Cabinet applet
- Added translations for Keys installer
- Other miscellaneous translations added
2024-12-06 21:57:35 -06:00
baad1e313f Stub Ldn.Lp2p.ISfService: 776 (DestroyGroup) (#353)
This prevents a crash in Mario Kart Live: Home Circuit that would occur
after exiting the kart pairing screen.
2024-12-06 14:43:31 -06:00
a1e6d11dcb Update Korean translation (#352) 2024-12-06 09:18:09 -06:00
3d168a8bfa direct errored updates to ryujinx.app 2024-12-06 08:18:24 -06:00
000c1756de version 1.2 in Info.plist 2024-12-06 08:17:04 -06:00
1d0152b961 UI: Move Shader Compilation hint, graphics backend, and GPU manufacturer to the right side of the status bar, next to firmware version.
Removed the "Game:" prefix in front of FPS.
2024-12-04 03:37:21 -06:00
07690e4527 chore: applets: Cleanup redundant ReadStruct implementations & provide a default implementation for IApplet#GetResult. 2024-12-04 02:24:40 -06:00
08b7257be5 Add the Cabinet Applet (#340)
This adds the missing Cabinet Applet, which allows for formatting
Amiibos and changing their names.
2024-12-02 23:40:02 -06:00
17483aad24 ARMeilleure: Allow TPIDR2_EL0 to be set properly (#339) 2024-12-02 14:42:07 -06:00
6b5cb151c3 Implement and stub services required for Mario Kart Live: Home Circuit (#331)
These changes allow Mario Kart Live: Home Circuit (v2.0.0) to boot into
menus. Kart functionality has not been implemented and will not work.

Version 1.0.0 is currently unsupported due to unimplemented ARM
registers. I plan on addressing this issue at a later date.


### Here is a list of the implemented and stubbed services in this PR:
#### Implemented:
Ldn.Lp2p.IServiceCreator: 0 (CreateNetworkService)
Ldn.Lp2p.IServiceCreator: 8 (CreateNetworkServiceMonitor)
Ldn.Lp2p.ISfService: 0 (Initialize)
Ldn.Lp2p.ISfServiceMonitor: 0 (Initialize)
Ldn.Lp2p.ISfServiceMonitor: 256 (AttachNetworkInterfaceStateChangeEvent)
Ldn.Lp2p.ISfServiceMonitor: 328 (AttachJoinEvent)
#### Stubbed:
Ldn.Lp2p.ISfService: 768 (CreateGroup)
Ldn.Lp2p.ISfService: 1536 (SendToOtherGroup)
Ldn.Lp2p.ISfService: 1544 (RecvFromOtherGroup)
Ldn.Lp2p.ISfServiceMonitor: 288 (GetGroupInfo)
Ldn.Lp2p.ISfServiceMonitor: 296 (GetGroupInfo2)
Ldn.Lp2p.ISfServiceMonitor: 312 (GetIpConfig)
2024-11-30 17:20:48 -06:00
3680df6092 Fix for missing text with specific system locale encoding (#330) 2024-11-30 17:17:30 -06:00
facc12a94a JIT Sparse Function Table random crash fix (#319)
A couple of games have random crashing with the JIT Sparse Ftable changes, and it seems to have been caused by an insufficient int size returned by `AddressTableLevel#GetValue(ulong address)`.
It was 32 bits (Int32), but the GiantBlock (which is the current address table impl) uses potentially 36 bits for the first level.
2024-11-29 16:32:55 -06:00
8e55e6d6d7 Korean translation for key install tool (#329) 2024-11-29 15:39:11 -06:00
346dfe9542 Added Tool for installing keys (#233)
#232 

![image](https://github.com/user-attachments/assets/5ae6118d-3857-4005-8392-5398c8fa91d5)
2024-11-28 17:32:07 -06:00
8a2b56cae6 Fix logic surrounding PushDescriptors in Vulkan (#257) 2024-11-28 17:00:12 -06:00
baf179efdb ignore macos attribute files (#302) 2024-11-28 16:55:51 -06:00
2a72fb2088 UI: RPC: Add Diablo III 2024-11-26 17:15:11 -06:00
0caeab2270 Remove 'Enter' hotkey in settings menu (#95)
This allows the Enter key to be bound to a button when using the
Avalonia UI.
2024-11-25 13:46:41 -06:00
f72d2c1b2b UI: Add Mii Edit Applet Locale (#311)
This allows the "Mii Edit Applet" dropdown to be localized ( I already
went ahead and localized French )
2024-11-25 13:43:01 -06:00
a18cecbc30 Korean "Show Changelog" translation (#313) 2024-11-25 13:40:39 -06:00
2e6794e69b Add custom refresh rate mode to VSync option (#238)
Rebased @jcm93's refreshinterval branch:
https://github.com/jcm93/Ryujinx/tree/refreshinterval

The option is placed under System/Hacks. Disabled, it's the default
Ryujinx behavior. Enabled, the behavior is shown in the attached
screenshots. If a framerate is too high or low, you can adjust the value
where you normally toggle VSync on and off. It will also cycle through
the default on/off toggles.

Also, in order to reduce clutter, I made an adjustment to remove the
target FPS and only show the percentage.

---------

Co-authored-by: jcm <6864788+jcm93@users.noreply.github.com>
2024-11-25 13:39:09 -06:00
7e16fccfc1 UI: Fix icons getting cutoff in the About window (#310)
Before:


![image](https://github.com/user-attachments/assets/c8d6b7d5-487b-4ab9-83e3-9489eaa0a076)

After:


![image](https://github.com/user-attachments/assets/18ea6360-f6ee-48e6-9a0a-cd8d88a0cf51)
2024-11-24 11:33:53 -06:00
a81212bbf1 Fix window decorations being too wide (#309) 2024-11-24 09:49:44 -06:00
140 changed files with 1989 additions and 403 deletions

3
.gitignore vendored
View File

@ -175,3 +175,6 @@ PublishProfiles/
# Glade backup files
*.glade~
# Ignore MacOS Attribute Files
._*

View File

@ -38,7 +38,7 @@
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="5.0.3-build14" />
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
<PackageVersion Include="Ryujinx.SDL2-CS" Version="2.30.0-build32" />
<PackageVersion Include="Gommon" Version="2.6.6" />
<PackageVersion Include="Gommon" Version="2.6.8" />
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
<PackageVersion Include="shaderc.net" Version="0.1.0" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" />

View File

@ -14,7 +14,7 @@ if [ -z "$RYUJINX_BIN" ]; then
exit 1
fi
COMMAND="env DOTNET_EnableAlternateStackCheck=1"
COMMAND="env LANG=C.UTF-8 DOTNET_EnableAlternateStackCheck=1"
if command -v gamemoderun > /dev/null 2>&1; then
COMMAND="$COMMAND gamemoderun"

View File

@ -40,11 +40,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.1</string>
<string>1.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.1.0</string>
<string>1.2.0</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>CSResourcesFileMapped</key>

View File

@ -17,7 +17,7 @@ error_handler() {
set the button_pressed to the button returned of the result
if the button_pressed is \"Open Download Page\" then
open location \"https://ryujinx.org/download\"
open location \"https://ryujinx.app/download\"
end if
"""
@ -54,4 +54,4 @@ if [ "$#" -le 3 ]; then
open -a "$INSTALL_DIRECTORY"
else
open -a "$INSTALL_DIRECTORY" --args "${APP_ARGUMENTS[@]}"
fi
fi

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -36,9 +36,9 @@ namespace ARMeilleure.Common
/// </summary>
/// <param name="address">Guest address</param>
/// <returns>Value of the <see cref="AddressTableLevel"/> from the specified guest <paramref name="address"/></returns>
public int GetValue(ulong address)
public long GetValue(ulong address)
{
return (int)((address & Mask) >> Index);
return (long)((address & Mask) >> Index);
}
}
}

View File

@ -88,7 +88,7 @@ namespace ARMeilleure.Instructions
EmitSetTpidrEl0(context);
return;
case 0b11_011_1101_0000_101:
EmitGetTpidr2El0(context);
EmitSetTpidr2El0(context);
return;
default:
@ -291,5 +291,16 @@ namespace ARMeilleure.Instructions
context.Store(context.Add(nativeContext, Const((ulong)NativeContext.GetTpidrEl0Offset())), value);
}
private static void EmitSetTpidr2El0(ArmEmitterContext context)
{
OpCodeSystem op = (OpCodeSystem)context.CurrOp;
Operand value = GetIntOrZR(context, op.Rt);
Operand nativeContext = context.LoadArgument(OperandType.I64, 0);
context.Store(context.Add(nativeContext, Const((ulong)NativeContext.GetTpidr2El0Offset())), value);
}
}
}

View File

@ -30,7 +30,7 @@ namespace ARMeilleure.Translation.PTC
private const string OuterHeaderMagicString = "PTCohd\0\0";
private const string InnerHeaderMagicString = "PTCihd\0\0";
private const uint InternalVersion = 6992; //! To be incremented manually for each change to the ARMeilleure project.
private const uint InternalVersion = 6997; //! To be incremented manually for each change to the ARMeilleure project.
private const string ActualDir = "0";
private const string BackupDir = "1";

View File

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RuntimeIdentifiers>win-x64;osx-x64;linux-x64</RuntimeIdentifiers>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -1,6 +1,8 @@
namespace Ryujinx.Common.Configuration.Hid.Controller
{
public class JoyconConfigControllerStick<TButton, TStick> where TButton : unmanaged where TStick : unmanaged
public class JoyconConfigControllerStick<TButton, TStick>
where TButton : unmanaged
where TStick : unmanaged
{
public TStick Joystick { get; set; }
public bool InvertStickX { get; set; }

View File

@ -2,7 +2,7 @@ namespace Ryujinx.Common.Configuration.Hid
{
public class KeyboardHotkeys
{
public Key ToggleVsync { get; set; }
public Key ToggleVSyncMode { get; set; }
public Key Screenshot { get; set; }
public Key ShowUI { get; set; }
public Key Pause { get; set; }
@ -11,5 +11,7 @@ namespace Ryujinx.Common.Configuration.Hid
public Key ResScaleDown { get; set; }
public Key VolumeUp { get; set; }
public Key VolumeDown { get; set; }
public Key CustomVSyncIntervalIncrement { get; set; }
public Key CustomVSyncIntervalDecrement { get; set; }
}
}

View File

@ -0,0 +1,9 @@
namespace Ryujinx.Common.Configuration
{
public enum VSyncMode
{
Switch,
Unbounded,
Custom
}
}

View File

@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefineConstants Condition=" '$(ExtraDefineConstants)' != '' ">$(DefineConstants);$(ExtraDefineConstants)</DefineConstants>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -238,7 +238,7 @@ namespace ARMeilleure.Common
{
TEntry* page = GetPage(address);
int index = Levels[^1].GetValue(address);
long index = Levels[^1].GetValue(address);
EnsureMapped((IntPtr)(page + index));

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -8,7 +8,7 @@ namespace Ryujinx.Graphics.GAL
void SetSize(int width, int height);
void ChangeVSyncMode(bool vsyncEnabled);
void ChangeVSyncMode(VSyncMode vSyncMode);
void SetAntiAliasing(AntiAliasing antialiasing);
void SetScalingFilter(ScalingFilter type);

View File

@ -31,7 +31,7 @@ namespace Ryujinx.Graphics.GAL.Multithreading
_impl.Window.SetSize(width, height);
}
public void ChangeVSyncMode(bool vsyncEnabled) { }
public void ChangeVSyncMode(VSyncMode vSyncMode) { }
public void SetAntiAliasing(AntiAliasing effect) { }

View File

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">

View File

@ -0,0 +1,9 @@
namespace Ryujinx.Graphics.GAL
{
public enum VSyncMode
{
Switch,
Unbounded,
Custom
}
}

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -54,7 +54,7 @@ namespace Ryujinx.Graphics.OpenGL
GL.PixelStore(PixelStoreParameter.UnpackAlignment, 4);
}
public void ChangeVSyncMode(bool vsyncEnabled) { }
public void ChangeVSyncMode(VSyncMode vSyncMode) { }
public void SetSize(int width, int height)
{

View File

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">

View File

@ -182,6 +182,16 @@ namespace Ryujinx.Graphics.Vulkan
return false;
}
}
//Prevent the sum of descriptors from exceeding MaxPushDescriptors
int totalDescriptors = 0;
foreach (ResourceDescriptor desc in layout.Sets.First().Descriptors)
{
if (!reserved.Contains(desc.Binding))
totalDescriptors += desc.Count;
}
if (totalDescriptors > gd.Capabilities.MaxPushDescriptors)
return false;
return true;
}

View File

@ -29,7 +29,7 @@ namespace Ryujinx.Graphics.Vulkan
private int _width;
private int _height;
private bool _vsyncEnabled;
private VSyncMode _vSyncMode;
private bool _swapchainIsDirty;
private VkFormat _format;
private AntiAliasing _currentAntiAliasing;
@ -139,7 +139,7 @@ namespace Ryujinx.Graphics.Vulkan
ImageArrayLayers = 1,
PreTransform = capabilities.CurrentTransform,
CompositeAlpha = ChooseCompositeAlpha(capabilities.SupportedCompositeAlpha),
PresentMode = ChooseSwapPresentMode(presentModes, _vsyncEnabled),
PresentMode = ChooseSwapPresentMode(presentModes, _vSyncMode),
Clipped = true,
};
@ -279,9 +279,9 @@ namespace Ryujinx.Graphics.Vulkan
}
}
private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, bool vsyncEnabled)
private static PresentModeKHR ChooseSwapPresentMode(PresentModeKHR[] availablePresentModes, VSyncMode vSyncMode)
{
if (!vsyncEnabled && availablePresentModes.Contains(PresentModeKHR.ImmediateKhr))
if (vSyncMode == VSyncMode.Unbounded && availablePresentModes.Contains(PresentModeKHR.ImmediateKhr))
{
return PresentModeKHR.ImmediateKhr;
}
@ -634,9 +634,10 @@ namespace Ryujinx.Graphics.Vulkan
_swapchainIsDirty = true;
}
public override void ChangeVSyncMode(bool vsyncEnabled)
public override void ChangeVSyncMode(VSyncMode vSyncMode)
{
_vsyncEnabled = vsyncEnabled;
_vSyncMode = vSyncMode;
//present mode may change, so mark the swapchain for recreation
_swapchainIsDirty = true;
}

View File

@ -10,7 +10,7 @@ namespace Ryujinx.Graphics.Vulkan
public abstract void Dispose();
public abstract void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback);
public abstract void SetSize(int width, int height);
public abstract void ChangeVSyncMode(bool vsyncEnabled);
public abstract void ChangeVSyncMode(VSyncMode vSyncMode);
public abstract void SetAntiAliasing(AntiAliasing effect);
public abstract void SetScalingFilter(ScalingFilter scalerType);
public abstract void SetScalingFilterLevel(float scale);

View File

@ -6,6 +6,7 @@
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
<IsRoslynComponent>true</IsRoslynComponent>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -21,6 +21,7 @@ using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Path = System.IO.Path;
namespace Ryujinx.HLE.FileSystem
@ -474,6 +475,74 @@ namespace Ryujinx.HLE.FileSystem
FinishInstallation(temporaryDirectory, registeredDirectory);
}
public void InstallKeys(string keysSource, string installDirectory)
{
if (Directory.Exists(keysSource))
{
foreach (var filePath in Directory.EnumerateFiles(keysSource, "*.keys"))
{
VerifyKeysFile(filePath);
File.Copy(filePath, Path.Combine(installDirectory, Path.GetFileName(filePath)), true);
}
return;
}
if (!File.Exists(keysSource))
{
throw new FileNotFoundException("Keys file does not exist.");
}
FileInfo info = new(keysSource);
using FileStream file = File.OpenRead(keysSource);
switch (info.Extension)
{
case ".zip":
using (ZipArchive archive = ZipFile.OpenRead(keysSource))
{
InstallKeysFromZip(archive, installDirectory);
}
break;
case ".keys":
VerifyKeysFile(keysSource);
File.Copy(keysSource, Path.Combine(installDirectory, info.Name), true);
break;
default:
throw new InvalidFirmwarePackageException("Input file is not a valid key package");
}
}
private void InstallKeysFromZip(ZipArchive archive, string installDirectory)
{
string temporaryDirectory = Path.Combine(installDirectory, "temp");
if (Directory.Exists(temporaryDirectory))
{
Directory.Delete(temporaryDirectory, true);
}
Directory.CreateDirectory(temporaryDirectory);
foreach (var entry in archive.Entries)
{
if (Path.GetExtension(entry.FullName).Equals(".keys", StringComparison.OrdinalIgnoreCase))
{
string extractDestination = Path.Combine(temporaryDirectory, entry.Name);
entry.ExtractToFile(extractDestination, overwrite: true);
try
{
VerifyKeysFile(extractDestination);
File.Move(extractDestination, Path.Combine(installDirectory, entry.Name), true);
}
catch (Exception)
{
Directory.Delete(temporaryDirectory, true);
throw;
}
}
}
Directory.Delete(temporaryDirectory, true);
}
private void FinishInstallation(string temporaryDirectory, string registeredDirectory)
{
if (Directory.Exists(registeredDirectory))
@ -947,5 +1016,70 @@ namespace Ryujinx.HLE.FileSystem
return null;
}
public void VerifyKeysFile(string filePath)
{
// Verify the keys file format refers to https://github.com/Thealexbarney/LibHac/blob/master/KEYS.md
string genericPattern = @"^[a-z0-9_]+ = [a-z0-9]+$";
string titlePattern = @"^[a-z0-9]{32} = [a-z0-9]{32}$";
if (File.Exists(filePath))
{
// Read all lines from the file
string fileName = Path.GetFileName(filePath);
string[] lines = File.ReadAllLines(filePath);
bool verified = false;
switch (fileName)
{
case "prod.keys":
verified = verifyKeys(lines, genericPattern);
break;
case "title.keys":
verified = verifyKeys(lines, titlePattern);
break;
case "console.keys":
verified = verifyKeys(lines, genericPattern);
break;
case "dev.keys":
verified = verifyKeys(lines, genericPattern);
break;
default:
throw new FormatException($"Keys file name \"{fileName}\" not supported. Only \"prod.keys\", \"title.keys\", \"console.keys\", \"dev.keys\" are supported.");
}
if (!verified)
{
throw new FormatException($"Invalid \"{filePath}\" file format.");
}
} else
{
throw new FileNotFoundException($"Keys file not found at \"{filePath}\".");
}
}
private bool verifyKeys(string[] lines, string regex)
{
foreach (string line in lines)
{
if (!Regex.IsMatch(line, regex))
{
return false;
}
}
return true;
}
public bool AreKeysAlredyPresent(string pathToCheck)
{
string[] fileNames = { "prod.keys", "title.keys", "console.keys", "dev.keys" };
foreach (var file in fileNames)
{
if (File.Exists(Path.Combine(pathToCheck, file)))
{
return true;
}
}
return false;
}
}
}

View File

@ -223,9 +223,10 @@ namespace Ryujinx.HLE.FileSystem
{
KeySet ??= KeySet.CreateDefaultKeySet();
string keyFile = null;
string prodKeyFile = null;
string titleKeyFile = null;
string consoleKeyFile = null;
string devKeyFile = null;
if (AppDataManager.Mode == AppDataManager.LaunchMode.UserProfile)
{
@ -236,13 +237,14 @@ namespace Ryujinx.HLE.FileSystem
void LoadSetAtPath(string basePath)
{
string localKeyFile = Path.Combine(basePath, "prod.keys");
string localProdKeyFile = Path.Combine(basePath, "prod.keys");
string localTitleKeyFile = Path.Combine(basePath, "title.keys");
string localConsoleKeyFile = Path.Combine(basePath, "console.keys");
string localDevKeyFile = Path.Combine(basePath, "dev.keys");
if (File.Exists(localKeyFile))
if (File.Exists(localProdKeyFile))
{
keyFile = localKeyFile;
prodKeyFile = localProdKeyFile;
}
if (File.Exists(localTitleKeyFile))
@ -254,9 +256,14 @@ namespace Ryujinx.HLE.FileSystem
{
consoleKeyFile = localConsoleKeyFile;
}
if (File.Exists(localDevKeyFile))
{
devKeyFile = localDevKeyFile;
}
}
ExternalKeyReader.ReadKeyFile(KeySet, keyFile, titleKeyFile, consoleKeyFile, null);
ExternalKeyReader.ReadKeyFile(KeySet, prodKeyFile, devKeyFile, titleKeyFile, consoleKeyFile, null);
}
public void ImportTickets(IFileSystem fs)

View File

@ -9,6 +9,7 @@ using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.SystemState;
using Ryujinx.HLE.UI;
using System;
using VSyncMode = Ryujinx.Common.Configuration.VSyncMode;
namespace Ryujinx.HLE
{
@ -84,9 +85,14 @@ namespace Ryujinx.HLE
internal readonly RegionCode Region;
/// <summary>
/// Control the initial state of the vertical sync in the SurfaceFlinger service.
/// Control the initial state of the present interval in the SurfaceFlinger service (previously Vsync).
/// </summary>
internal readonly bool EnableVsync;
internal readonly VSyncMode VSyncMode;
/// <summary>
/// Control the custom VSync interval, if enabled and active.
/// </summary>
internal readonly int CustomVSyncInterval;
/// <summary>
/// Control the initial state of the docked mode.
@ -195,7 +201,7 @@ namespace Ryujinx.HLE
IHostUIHandler hostUIHandler,
SystemLanguage systemLanguage,
RegionCode region,
bool enableVsync,
VSyncMode vSyncMode,
bool enableDockedMode,
bool enablePtc,
bool enableInternetAccess,
@ -212,7 +218,8 @@ namespace Ryujinx.HLE
MultiplayerMode multiplayerMode,
bool multiplayerDisableP2p,
string multiplayerLdnPassphrase,
string multiplayerLdnServer)
string multiplayerLdnServer,
int customVSyncInterval)
{
VirtualFileSystem = virtualFileSystem;
LibHacHorizonManager = libHacHorizonManager;
@ -225,7 +232,8 @@ namespace Ryujinx.HLE
HostUIHandler = hostUIHandler;
SystemLanguage = systemLanguage;
Region = region;
EnableVsync = enableVsync;
VSyncMode = vSyncMode;
CustomVSyncInterval = customVSyncInterval;
EnableDockedMode = enableDockedMode;
EnablePtc = enablePtc;
EnableInternetAccess = enableInternetAccess;

View File

@ -1,5 +1,6 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Applets.Browser;
using Ryujinx.HLE.HOS.Applets.Cabinet;
using Ryujinx.HLE.HOS.Applets.Dummy;
using Ryujinx.HLE.HOS.Applets.Error;
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
@ -23,14 +24,14 @@ namespace Ryujinx.HLE.HOS.Applets
case AppletId.SoftwareKeyboard:
return new SoftwareKeyboardApplet(system);
case AppletId.LibAppletWeb:
return new BrowserApplet(system);
case AppletId.LibAppletShop:
return new BrowserApplet(system);
case AppletId.LibAppletOff:
return new BrowserApplet(system);
return new BrowserApplet();
case AppletId.MiiEdit:
Logger.Warning?.Print(LogClass.Application, $"Please use the MiiEdit inside File/Open Applet");
return new DummyApplet(system);
case AppletId.Cabinet:
return new CabinetApplet(system);
}
Logger.Warning?.Print(LogClass.Application, $"Applet {applet} not implemented!");

View File

@ -18,13 +18,6 @@ namespace Ryujinx.HLE.HOS.Applets.Browser
private List<BrowserArgument> _arguments;
private ShimKind _shimKind;
public BrowserApplet(Horizon system) { }
public ResultCode GetResult()
{
return ResultCode.Success;
}
public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession)
{
_normalSession = normalSession;

View File

@ -0,0 +1,182 @@
using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.HLE.HOS.Services.Am.AppletAE;
using Ryujinx.HLE.HOS.Services.Hid.HidServer;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.HLE.HOS.Services.Nfc.Nfp;
using Ryujinx.HLE.HOS.Services.Nfc.Nfp.NfpManager;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
namespace Ryujinx.HLE.HOS.Applets.Cabinet
{
internal unsafe class CabinetApplet : IApplet
{
private readonly Horizon _system;
private AppletSession _normalSession;
public event EventHandler AppletStateChanged;
public CabinetApplet(Horizon system)
{
_system = system;
}
public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession)
{
_normalSession = normalSession;
byte[] launchParams = _normalSession.Pop();
byte[] startParamBytes = _normalSession.Pop();
StartParamForAmiiboSettings startParam = IApplet.ReadStruct<StartParamForAmiiboSettings>(startParamBytes);
Logger.Stub?.PrintStub(LogClass.ServiceAm, $"CabinetApplet Start Type: {startParam.Type}");
switch (startParam.Type)
{
case 0:
StartNicknameAndOwnerSettings(ref startParam);
break;
case 1:
case 3:
StartFormatter(ref startParam);
break;
default:
Logger.Error?.Print(LogClass.ServiceAm, $"Unknown AmiiboSettings type: {startParam.Type}");
break;
}
// Prepare the response
ReturnValueForAmiiboSettings returnValue = new()
{
AmiiboSettingsReturnFlag = (byte)AmiiboSettingsReturnFlag.HasRegisterInfo,
DeviceHandle = new DeviceHandle
{
Handle = 0 // Dummy device handle
},
RegisterInfo = startParam.RegisterInfo
};
// Push the response
_normalSession.Push(BuildResponse(returnValue));
AppletStateChanged?.Invoke(this, null);
_system.ReturnFocus();
return ResultCode.Success;
}
public ResultCode GetResult()
{
_system.Device.System.NfpDevices.RemoveAt(0);
return ResultCode.Success;
}
private void StartFormatter(ref StartParamForAmiiboSettings startParam)
{
// Initialize RegisterInfo
startParam.RegisterInfo = new RegisterInfo();
}
private void StartNicknameAndOwnerSettings(ref StartParamForAmiiboSettings startParam)
{
_system.Device.UIHandler.DisplayCabinetDialog(out string newName);
byte[] nameBytes = Encoding.UTF8.GetBytes(newName);
Array41<byte> nickName = new Array41<byte>();
nameBytes.CopyTo(nickName.AsSpan());
startParam.RegisterInfo.Nickname = nickName;
NfpDevice devicePlayer1 = new()
{
NpadIdType = NpadIdType.Player1,
Handle = HidUtils.GetIndexFromNpadIdType(NpadIdType.Player1),
State = NfpDeviceState.SearchingForTag,
};
_system.Device.System.NfpDevices.Add(devicePlayer1);
_system.Device.UIHandler.DisplayCabinetMessageDialog();
string amiiboId = string.Empty;
bool scanned = false;
while (!scanned)
{
for (int i = 0; i < _system.Device.System.NfpDevices.Count; i++)
{
if (_system.Device.System.NfpDevices[i].State == NfpDeviceState.TagFound)
{
amiiboId = _system.Device.System.NfpDevices[i].AmiiboId;
scanned = true;
}
}
}
VirtualAmiibo.UpdateNickName(amiiboId, newName);
}
private static byte[] BuildResponse(ReturnValueForAmiiboSettings returnValue)
{
int size = Unsafe.SizeOf<ReturnValueForAmiiboSettings>();
byte[] bytes = new byte[size];
fixed (byte* bytesPtr = bytes)
{
Unsafe.Write(bytesPtr, returnValue);
}
return bytes;
}
#region Structs
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public unsafe struct TagInfo
{
public fixed byte Data[0x58];
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public unsafe struct StartParamForAmiiboSettings
{
public byte ZeroValue; // Left at zero by sdknso
public byte Type;
public byte Flags;
public byte AmiiboSettingsStartParamOffset28;
public ulong AmiiboSettingsStartParam0;
public TagInfo TagInfo; // Only enabled when flags bit 1 is set
public RegisterInfo RegisterInfo; // Only enabled when flags bit 2 is set
public fixed byte StartParamExtraData[0x20];
public fixed byte Reserved[0x24];
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public unsafe struct ReturnValueForAmiiboSettings
{
public byte AmiiboSettingsReturnFlag;
private byte Padding1;
private byte Padding2;
private byte Padding3;
public DeviceHandle DeviceHandle;
public TagInfo TagInfo;
public RegisterInfo RegisterInfo;
public fixed byte IgnoredBySdknso[0x24];
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct DeviceHandle
{
public ulong Handle;
}
public enum AmiiboSettingsReturnFlag : byte
{
Cancel = 0,
HasTagInfo = 2,
HasRegisterInfo = 4,
HasTagInfoAndRegisterInfo = 6
}
#endregion
}
}

View File

@ -117,11 +117,6 @@ namespace Ryujinx.HLE.HOS.Applets
return ResultCode.Success;
}
public ResultCode GetResult()
{
return ResultCode.Success;
}
private static byte[] BuildResponse(ControllerSupportResultInfo result)
{
using MemoryStream stream = MemoryStreamManager.Shared.GetStream();

View File

@ -11,11 +11,14 @@ namespace Ryujinx.HLE.HOS.Applets.Dummy
{
private readonly Horizon _system;
private AppletSession _normalSession;
public event EventHandler AppletStateChanged;
public DummyApplet(Horizon system)
{
_system = system;
}
public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession)
{
_normalSession = normalSession;
@ -24,10 +27,7 @@ namespace Ryujinx.HLE.HOS.Applets.Dummy
_system.ReturnFocus();
return ResultCode.Success;
}
private static T ReadStruct<T>(byte[] data) where T : struct
{
return MemoryMarshal.Read<T>(data.AsSpan());
}
private static byte[] BuildResponse()
{
using MemoryStream stream = MemoryStreamManager.Shared.GetStream();
@ -35,9 +35,5 @@ namespace Ryujinx.HLE.HOS.Applets.Dummy
writer.Write((ulong)ResultCode.Success);
return stream.ToArray();
}
public ResultCode GetResult()
{
return ResultCode.Success;
}
}
}

View File

@ -203,10 +203,5 @@ namespace Ryujinx.HLE.HOS.Applets.Error
_horizon.Device.UIHandler.DisplayErrorAppletDialog($"Error Number: {applicationErrorArg.ErrorNumber} (Details)", "\n" + detailsText, buttons.ToArray());
}
}
public ResultCode GetResult()
{
return ResultCode.Success;
}
}
}

View File

@ -13,7 +13,7 @@ namespace Ryujinx.HLE.HOS.Applets
ResultCode Start(AppletSession normalSession,
AppletSession interactiveSession);
ResultCode GetResult();
ResultCode GetResult() => ResultCode.Success;
bool DrawTo(RenderingSurfaceInfo surfaceInfo, IVirtualMemoryManager destination, ulong position) => false;

View File

@ -37,11 +37,6 @@ namespace Ryujinx.HLE.HOS.Applets
return ResultCode.Success;
}
public ResultCode GetResult()
{
return ResultCode.Success;
}
private byte[] BuildResponse()
{
UserProfile currentUser = _system.AccountManager.LastOpenedUser;

View File

@ -144,11 +144,6 @@ namespace Ryujinx.HLE.HOS.Applets
}
}
public ResultCode GetResult()
{
return ResultCode.Success;
}
private bool IsKeyboardActive()
{
return _backgroundState >= InlineKeyboardState.Appearing && _backgroundState < InlineKeyboardState.Disappearing;

View File

@ -2,7 +2,7 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
/// <summary>
/// Wraps a type in a class so it gets stored in the GC managed heap. This is used as communication mechanism
/// between classed that need to be disposed and, thus, can't share their references.
/// between classes that need to be disposed and, thus, can't share their references.
/// </summary>
/// <typeparam name="T">The internal type.</typeparam>
class TRef<T>

View File

@ -5,5 +5,23 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.Lp2p
class IServiceCreator : IpcService
{
public IServiceCreator(ServiceCtx context) { }
[CommandCmif(0)]
// CreateNetworkService(pid, u64, u32) -> object<nn::ldn::detail::ISfService>
public ResultCode CreateNetworkService(ServiceCtx context)
{
MakeObject(context, new ISfService(context));
return ResultCode.Success;
}
[CommandCmif(8)]
// CreateNetworkServiceMonitor(pid, u64) -> object<nn::ldn::detail::ISfServiceMonitor>
public ResultCode CreateNetworkServiceMonitor(ServiceCtx context)
{
MakeObject(context, new ISfServiceMonitor(context));
return ResultCode.Success;
}
}
}

View File

@ -0,0 +1,54 @@
using Ryujinx.Common.Logging;
namespace Ryujinx.HLE.HOS.Services.Ldn.Lp2p
{
class ISfService : IpcService
{
public ISfService(ServiceCtx context) { }
[CommandCmif(0)]
// Initialize()
public ResultCode Initialize(ServiceCtx context)
{
context.ResponseData.Write(0);
return ResultCode.Success;
}
[CommandCmif(768)]
// CreateGroup(buffer<nn::lp2p::GroupInfo, 0x31)
public ResultCode CreateGroup(ServiceCtx context)
{
Logger.Stub?.PrintStub(LogClass.ServiceLdn);
return ResultCode.Success;
}
[CommandCmif(776)]
// DestroyGroup()
public ResultCode DestroyGroup(ServiceCtx context)
{
Logger.Stub?.PrintStub(LogClass.ServiceLdn);
return ResultCode.Success;
}
[CommandCmif(1536)]
// SendToOtherGroup(nn::lp2p::MacAddress, nn::lp2p::GroupId, s16, s16, u32, buffer<unknown, 0x21>)
public ResultCode SendToOtherGroup(ServiceCtx context)
{
Logger.Stub?.PrintStub(LogClass.ServiceLdn);
return ResultCode.Success;
}
[CommandCmif(1544)]
// RecvFromOtherGroup(u32, buffer<unknown, 0x22>) -> (nn::lp2p::MacAddress, u16, s16, u32, s32)
public ResultCode RecvFromOtherGroup(ServiceCtx context)
{
Logger.Stub?.PrintStub(LogClass.ServiceLdn);
return ResultCode.Success;
}
}
}

View File

@ -0,0 +1,86 @@
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS.Ipc;
using Ryujinx.HLE.HOS.Kernel.Threading;
using Ryujinx.Horizon.Common;
using System;
namespace Ryujinx.HLE.HOS.Services.Ldn.Lp2p
{
class ISfServiceMonitor : IpcService
{
private readonly KEvent _stateChangeEvent;
private readonly KEvent _jointEvent;
private int _stateChangeEventHandle = 0;
private int _jointEventHandle = 0;
public ISfServiceMonitor(ServiceCtx context)
{
_stateChangeEvent = new KEvent(context.Device.System.KernelContext);
_jointEvent = new KEvent(context.Device.System.KernelContext);
}
[CommandCmif(0)]
// Initialize()
public ResultCode Initialize(ServiceCtx context)
{
context.ResponseData.Write(0);
return ResultCode.Success;
}
[CommandCmif(256)]
// AttachNetworkInterfaceStateChangeEvent() -> handle<copy>
public ResultCode AttachNetworkInterfaceStateChangeEvent(ServiceCtx context)
{
if (context.Process.HandleTable.GenerateHandle(_stateChangeEvent.ReadableEvent, out _stateChangeEventHandle) != Result.Success)
{
throw new InvalidOperationException("Out of handles!");
}
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_stateChangeEventHandle);
return ResultCode.Success;
}
[CommandCmif(288)]
// GetGroupInfo(buffer<nn::lp2p::GroupInfo, 0x32>)
public ResultCode GetGroupInfo(ServiceCtx context)
{
Logger.Stub?.PrintStub(LogClass.ServiceLdn);
return ResultCode.Success;
}
[CommandCmif(296)]
// GetGroupInfo2(buffer<nn::lp2p::GroupInfo, 0x32>, buffer<nn::lp2p::GroupInfo, 0x31>)
public ResultCode GetGroupInfo2(ServiceCtx context)
{
Logger.Stub?.PrintStub(LogClass.ServiceLdn);
return ResultCode.Success;
}
[CommandCmif(312)]
// GetIpConfig(buffer<unknown<0x100>, 0x1a>)
public ResultCode GetIpConfig(ServiceCtx context)
{
Logger.Stub?.PrintStub(LogClass.ServiceLdn);
return ResultCode.Success;
}
[CommandCmif(328)]
// AttachNetworkInterfaceStateChangeEvent() -> handle<copy>
public ResultCode AttachJoinEvent(ServiceCtx context)
{
if (context.Process.HandleTable.GenerateHandle(_jointEvent.ReadableEvent, out _jointEventHandle) != Result.Success)
{
throw new InvalidOperationException("Out of handles!");
}
context.Response.HandleDesc = IpcHandleDesc.MakeCopy(_jointEventHandle);
return ResultCode.Success;
}
}
}

View File

@ -1,3 +1,5 @@
using Gommon;
using Humanizer;
using NetCoreServer;
using Open.Nat;
using Ryujinx.Common.Logging;
@ -153,7 +155,10 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
if (_publicPort != 0)
{
_ = Task.Delay(PortLeaseRenew * 1000, _disposedCancellation.Token).ContinueWith((task) => Task.Run(RefreshLease));
_ = Executor.ExecuteAfterDelayAsync(
PortLeaseRenew.Seconds(),
_disposedCancellation.Token,
RefreshLease);
}
_natDevice = device;
@ -257,7 +262,10 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy
}
_ = Task.Delay(PortLeaseRenew, _disposedCancellation.Token).ContinueWith((task) => Task.Run(RefreshLease));
_ = Executor.ExecuteAfterDelayAsync(
PortLeaseRenew.Milliseconds(),
_disposedCancellation.Token,
RefreshLease);
}
public bool TryRegisterUser(P2pProxySession session, ExternalProxyConfig config)

View File

@ -93,6 +93,13 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp
return registerInfo;
}
public static void UpdateNickName(string amiiboId, string newNickName)
{
VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId);
virtualAmiiboFile.NickName = newNickName;
SaveAmiiboFile(virtualAmiiboFile);
}
public static bool OpenApplicationArea(string amiiboId, uint applicationAreaId)
{
VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId);

View File

@ -10,13 +10,12 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using VSyncMode = Ryujinx.Common.Configuration.VSyncMode;
namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
{
class SurfaceFlinger : IConsumerListener, IDisposable
{
private const int TargetFps = 60;
private readonly Switch _device;
private readonly Dictionary<long, Layer> _layers;
@ -32,6 +31,9 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
private readonly long _spinTicks;
private readonly long _1msTicks;
private VSyncMode _vSyncMode;
private long _targetVSyncInterval;
private int _swapInterval;
private int _swapIntervalDelay;
@ -88,7 +90,8 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
}
else
{
_ticksPerFrame = Stopwatch.Frequency / TargetFps;
_ticksPerFrame = Stopwatch.Frequency / _device.TargetVSyncInterval;
_targetVSyncInterval = _device.TargetVSyncInterval;
}
}
@ -370,15 +373,20 @@ namespace Ryujinx.HLE.HOS.Services.SurfaceFlinger
if (acquireStatus == Status.Success)
{
// If device vsync is disabled, reflect the change.
if (!_device.EnableDeviceVsync)
if (_device.VSyncMode == VSyncMode.Unbounded)
{
if (_swapInterval != 0)
{
UpdateSwapInterval(0);
_vSyncMode = _device.VSyncMode;
}
}
else if (item.SwapInterval != _swapInterval)
else if (_device.VSyncMode != _vSyncMode)
{
UpdateSwapInterval(_device.VSyncMode == VSyncMode.Unbounded ? 0 : item.SwapInterval);
_vSyncMode = _device.VSyncMode;
}
else if (item.SwapInterval != _swapInterval || _device.TargetVSyncInterval != _targetVSyncInterval)
{
UpdateSwapInterval(item.SwapInterval);
}

View File

@ -26,7 +26,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
{
private static readonly TitleUpdateMetadataJsonSerializerContext _applicationSerializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca)
public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca, BlitStruct<ApplicationControlProperty>? customNacpData = null)
{
// Extract RomFs and ExeFs from NCA.
IStorage romFs = nca.GetRomFs(device, patchNca);
@ -55,6 +55,10 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
{
nacpData = controlNca.GetNacp(device);
}
else if (customNacpData != null) // if the Application doesn't provide a nacp file but the Application provides an override, use the provided nacp override
{
nacpData = (BlitStruct<ApplicationControlProperty>)customNacpData;
}
/* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" non-existent update.

View File

@ -98,12 +98,12 @@ namespace Ryujinx.HLE.Loaders.Processes
return false;
}
public bool LoadNca(string path)
public bool LoadNca(string path, BlitStruct<ApplicationControlProperty>? customNacpData = null)
{
FileStream file = new(path, FileMode.Open, FileAccess.Read);
Nca nca = new(_device.Configuration.VirtualFileSystem.KeySet, file.AsStorage(false));
ProcessResult processResult = nca.Load(_device, null, null);
ProcessResult processResult = nca.Load(_device, null, null, customNacpData);
if (processResult.ProcessId != 0 && _processesByPid.TryAdd(processResult.ProcessId, processResult))
{

View File

@ -84,12 +84,19 @@ namespace Ryujinx.HLE.Loaders.Processes
return false;
}
bool isFirmware = ProgramId is >= 0x0100000000000819 and <= 0x010000000000081C;
bool isFirmwareApplication = ProgramId <= 0x0100000000007FFF;
string name = !isFirmware
? (isFirmwareApplication ? "Firmware Application " : "") + (!string.IsNullOrWhiteSpace(Name) ? Name : "<Unknown Name>")
: "Firmware";
// TODO: LibHac npdm currently doesn't support version field.
string version = ProgramId > 0x0100000000007FFF
? DisplayVersion
string version = !isFirmware
? (!string.IsNullOrWhiteSpace(DisplayVersion) ? DisplayVersion : "<Unknown Version>")
: device.System.ContentManager.GetCurrentFirmwareVersion()?.VersionString ?? "?";
Logger.Info?.Print(LogClass.Loader, $"Application Loaded: {Name} v{version} [{ProgramIdText}] [{(Is64Bit ? "64-bit" : "32-bit")}]");
Logger.Info?.Print(LogClass.Loader, $"Application Loaded: {name} v{version} [{ProgramIdText}] [{(Is64Bit ? "64-bit" : "32-bit")}]");
return true;
}

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -0,0 +1,37 @@
using LibHac.Common;
using LibHac.Ns;
using System;
using System.Text;
namespace Ryujinx.HLE
{
public static class StructHelpers
{
public static BlitStruct<ApplicationControlProperty> CreateCustomNacpData(string name, string version)
{
// https://switchbrew.org/wiki/NACP
const int OffsetOfDisplayVersion = 0x3060;
// https://switchbrew.org/wiki/NACP#ApplicationTitle
const int TotalApplicationTitles = 0x10;
const int SizeOfApplicationTitle = 0x300;
const int OffsetOfApplicationPublisherStrings = 0x200;
var nacpData = new BlitStruct<ApplicationControlProperty>(1);
// name and publisher buffer
// repeat once for each locale (the ApplicationControlProperty has 16 locales)
for (int i = 0; i < TotalApplicationTitles; i++)
{
Encoding.ASCII.GetBytes(name).AsSpan().CopyTo(nacpData.ByteSpan[(i * SizeOfApplicationTitle)..]);
"Ryujinx"u8.CopyTo(nacpData.ByteSpan[(i * SizeOfApplicationTitle + OffsetOfApplicationPublisherStrings)..]);
}
// version buffer
Encoding.ASCII.GetBytes(version).AsSpan().CopyTo(nacpData.ByteSpan[OffsetOfDisplayVersion..]);
return nacpData;
}
}
}

View File

@ -1,3 +1,5 @@
using LibHac.Common;
using LibHac.Ns;
using Ryujinx.Audio.Backends.CompatLayer;
using Ryujinx.Audio.Integration;
using Ryujinx.Common.Configuration;
@ -27,7 +29,11 @@ namespace Ryujinx.HLE
public TamperMachine TamperMachine { get; }
public IHostUIHandler UIHandler { get; }
public bool EnableDeviceVsync { get; set; }
public VSyncMode VSyncMode { get; set; } = VSyncMode.Switch;
public bool CustomVSyncIntervalEnabled { get; set; } = false;
public int CustomVSyncInterval { get; set; }
public long TargetVSyncInterval { get; set; } = 60;
public bool IsFrameAvailable => Gpu.Window.IsFrameAvailable;
@ -59,12 +65,14 @@ namespace Ryujinx.HLE
System.State.SetLanguage(Configuration.SystemLanguage);
System.State.SetRegion(Configuration.Region);
EnableDeviceVsync = Configuration.EnableVsync;
VSyncMode = Configuration.VSyncMode;
CustomVSyncInterval = Configuration.CustomVSyncInterval;
System.State.DockedMode = Configuration.EnableDockedMode;
System.PerformanceState.PerformanceMode = System.State.DockedMode ? PerformanceMode.Boost : PerformanceMode.Default;
System.EnablePtc = Configuration.EnablePtc;
System.FsIntegrityCheckLevel = Configuration.FsIntegrityCheckLevel;
System.GlobalAccessLogMode = Configuration.FsGlobalAccessLogMode;
UpdateVSyncInterval();
#pragma warning restore IDE0055
}
@ -75,9 +83,37 @@ namespace Ryujinx.HLE
Gpu.GPFifo.DispatchCalls();
}
public void IncrementCustomVSyncInterval()
{
CustomVSyncInterval += 1;
UpdateVSyncInterval();
}
public void DecrementCustomVSyncInterval()
{
CustomVSyncInterval -= 1;
UpdateVSyncInterval();
}
public void UpdateVSyncInterval()
{
switch (VSyncMode)
{
case VSyncMode.Custom:
TargetVSyncInterval = CustomVSyncInterval;
break;
case VSyncMode.Switch:
TargetVSyncInterval = 60;
break;
case VSyncMode.Unbounded:
TargetVSyncInterval = 1;
break;
}
}
public bool LoadCart(string exeFsDir, string romFsFile = null) => Processes.LoadUnpackedNca(exeFsDir, romFsFile);
public bool LoadXci(string xciFile, ulong applicationId = 0) => Processes.LoadXci(xciFile, applicationId);
public bool LoadNca(string ncaFile) => Processes.LoadNca(ncaFile);
public bool LoadNca(string ncaFile, BlitStruct<ApplicationControlProperty>? customNacpData = null) => Processes.LoadNca(ncaFile, customNacpData);
public bool LoadNsp(string nspFile, ulong applicationId = 0) => Processes.LoadNsp(nspFile, applicationId);
public bool LoadProgram(string fileName) => Processes.LoadNxo(fileName);

View File

@ -24,6 +24,18 @@ namespace Ryujinx.HLE.UI
/// <returns>True when OK is pressed, False otherwise.</returns>
bool DisplayMessageDialog(ControllerAppletUIArgs args);
/// <summary>
/// Displays an Input Dialog box to the user so they can enter the Amiibo's new name
/// </summary>
/// <param name="userText">Text that the user entered. Set to `null` on internal errors</param>
/// <returns>True when OK is pressed, False otherwise. Also returns True on internal errors</returns>
bool DisplayCabinetDialog(out string userText);
/// <summary>
/// Displays a Message Dialog box to the user to notify them to scan the Amiibo.
/// </summary>
void DisplayCabinetMessageDialog();
/// <summary>
/// Tell the UI that we need to transition to another program.
/// </summary>

View File

@ -115,8 +115,11 @@ namespace Ryujinx.Headless.SDL2
[Option("fs-global-access-log-mode", Required = false, Default = 0, HelpText = "Enables FS access log output to the console.")]
public int FsGlobalAccessLogMode { get; set; }
[Option("disable-vsync", Required = false, HelpText = "Disables Vertical Sync.")]
public bool DisableVSync { get; set; }
[Option("vsync-mode", Required = false, Default = VSyncMode.Switch, HelpText = "Sets the emulated VSync mode (Switch, Unbounded, or Custom).")]
public VSyncMode VSyncMode { get; set; }
[Option("custom-refresh-rate", Required = false, Default = 90, HelpText = "Sets the custom refresh rate target value (integer).")]
public int CustomVSyncInterval { get; set; }
[Option("disable-shader-cache", Required = false, HelpText = "Disables Shader cache.")]
public bool DisableShaderCache { get; set; }

View File

@ -1,4 +1,5 @@
using CommandLine;
using Gommon;
using LibHac.Tools.FsSystem;
using Ryujinx.Audio.Backends.SDL2;
using Ryujinx.Common;
@ -96,8 +97,13 @@ namespace Ryujinx.Headless.SDL2
}
Parser.Default.ParseArguments<Options>(args)
.WithParsed(Load)
.WithNotParsed(errors => errors.Output());
.WithParsed(Load)
.WithNotParsed(errors =>
{
Logger.Error?.PrintMsg(LogClass.Application, "Error parsing command-line arguments:");
errors.ForEach(err => Logger.Error?.PrintMsg(LogClass.Application, $" - {err.Tag}"));
});
}
private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index)
@ -563,7 +569,7 @@ namespace Ryujinx.Headless.SDL2
window,
options.SystemLanguage,
options.SystemRegion,
!options.DisableVSync,
options.VSyncMode,
!options.DisableDockedMode,
!options.DisablePTC,
options.EnableInternetAccess,
@ -579,8 +585,9 @@ namespace Ryujinx.Headless.SDL2
options.MultiplayerLanInterfaceId,
Common.Configuration.Multiplayer.MultiplayerMode.Disabled,
false,
"",
"");
string.Empty,
string.Empty,
options.CustomVSyncInterval);
return new Switch(configuration);
}

View File

@ -9,6 +9,7 @@
<DefineConstants Condition=" '$(ExtraDefineConstants)' != '' ">$(DefineConstants);$(ExtraDefineConstants)</DefineConstants>
<SigningCertificate Condition=" '$(SigningCertificate)' == '' ">-</SigningCertificate>
<TieredPGO>true</TieredPGO>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -3,7 +3,7 @@ using System;
namespace Ryujinx.Headless.SDL2
{
class StatusUpdatedEventArgs(
bool vSyncEnabled,
string vSyncMode,
string dockedMode,
string aspectRatio,
string gameStatus,
@ -11,7 +11,7 @@ namespace Ryujinx.Headless.SDL2
string gpuName)
: EventArgs
{
public bool VSyncEnabled = vSyncEnabled;
public string VSyncMode = vSyncMode;
public string DockedMode = dockedMode;
public string AspectRatio = aspectRatio;
public string GameStatus = gameStatus;

View File

@ -1,4 +1,5 @@
using Humanizer;
using LibHac.Tools.Fs;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging;
@ -314,7 +315,7 @@ namespace Ryujinx.Headless.SDL2
}
StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
Device.EnableDeviceVsync,
Device.VSyncMode.ToString(),
dockedMode,
Device.Configuration.AspectRatio.ToText(),
$"Game: {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
@ -485,6 +486,19 @@ namespace Ryujinx.Headless.SDL2
return true;
}
public bool DisplayCabinetDialog(out string userText)
{
// SDL2 doesn't support input dialogs
userText = "Ryujinx";
return true;
}
public void DisplayCabinetMessageDialog()
{
SDL_ShowSimpleMessageBox(SDL_MessageBoxFlags.SDL_MESSAGEBOX_INFORMATION, "Cabinet Dialog", "Please scan your Amiibo now.", WindowHandle);
}
public bool DisplayMessageDialog(ControllerAppletUIArgs args)
{
if (_ignoreControllerApplet) return false;

View File

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -3,6 +3,8 @@
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<OutputType>Exe</OutputType>
<Configurations>Debug;Release</Configurations>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -4,6 +4,7 @@
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Configurations>Debug;Release</Configurations>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<PropertyGroup>

View File

@ -10,6 +10,7 @@
<TargetOS Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true'">linux</TargetOS>
<Configurations>Debug;Release</Configurations>
<RunSettingsFilePath>$(MSBuildProjectDirectory)\.runsettings</RunSettingsFilePath>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<PropertyGroup>

View File

@ -1,5 +1,4 @@
using DynamicData;
using DynamicData.Kernel;
using Gommon;
using LibHac;
using LibHac.Common;
@ -37,14 +36,13 @@ using System.Threading.Tasks;
using ContentType = LibHac.Ncm.ContentType;
using MissingKeyException = LibHac.Common.Keys.MissingKeyException;
using Path = System.IO.Path;
using SpanHelpers = LibHac.Common.SpanHelpers;
using TimeSpan = System.TimeSpan;
namespace Ryujinx.UI.App.Common
{
public class ApplicationLibrary
{
public static string DefaultLanPlayWebHost = "ryuldnweb.vudjun.com";
public const string DefaultLanPlayWebHost = "ryuldnweb.vudjun.com";
public Language DesiredLanguage { get; set; }
public event EventHandler<ApplicationCountUpdatedEventArgs> ApplicationCountUpdated;
public event EventHandler<LdnGameDataReceivedEventArgs> LdnGameDataReceived;
@ -191,12 +189,9 @@ namespace Ryujinx.UI.App.Common
}
}
if (isExeFs)
{
return GetApplicationFromExeFs(pfs, filePath);
}
return null;
return isExeFs
? GetApplicationFromExeFs(pfs, filePath)
: null;
}
/// <exception cref="LibHac.Common.Keys.MissingKeyException">The configured key set is missing a key.</exception>
@ -512,10 +507,6 @@ namespace Ryujinx.UI.App.Common
case ".xci":
case ".nsp":
{
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
? IntegrityCheckLevel.ErrorOnInvalid
: IntegrityCheckLevel.None;
using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(filePath, _virtualFileSystem);
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
@ -604,7 +595,7 @@ namespace Ryujinx.UI.App.Common
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),
nacpFile.Get.Read(out _, 0, LibHac.Common.SpanHelpers.AsByteSpan(ref controlData),
ReadOption.None).ThrowIfFailure();
var displayVersion = controlData.DisplayVersionString.ToString();
@ -827,7 +818,7 @@ namespace Ryujinx.UI.App.Common
{
_downloadableContents.Edit(it =>
{
DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, application.IdBase, dlcs);
DownloadableContentsHelper.SaveDownloadableContentsJson(application.IdBase, dlcs);
it.Remove(it.Items.Where(item => item.Dlc.TitleIdBase == application.IdBase));
it.AddOrUpdate(dlcs);
@ -839,7 +830,7 @@ namespace Ryujinx.UI.App.Common
{
_titleUpdates.Edit(it =>
{
TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, application.IdBase, updates);
TitleUpdatesHelper.SaveTitleUpdatesJson(application.IdBase, updates);
it.Remove(it.Items.Where(item => item.TitleUpdate.TitleIdBase == application.IdBase));
it.AddOrUpdate(updates);
@ -1088,14 +1079,15 @@ namespace Ryujinx.UI.App.Common
private bool AddAndAutoSelectUpdate(TitleUpdateModel update)
{
var currentlySelected = TitleUpdates.Items.FirstOrOptional(it =>
if (update == null) return false;
var currentlySelected = TitleUpdates.Items.FindFirst(it =>
it.TitleUpdate.TitleIdBase == update.TitleIdBase && it.IsSelected);
var shouldSelect = !currentlySelected.HasValue ||
currentlySelected.Value.TitleUpdate.Version < update.Version;
var shouldSelect = currentlySelected.Check(curr => curr.TitleUpdate?.Version < update.Version);
_titleUpdates.AddOrUpdate((update, shouldSelect));
if (currentlySelected.HasValue && shouldSelect)
{
_titleUpdates.AddOrUpdate((currentlySelected.Value.TitleUpdate, false));
@ -1464,7 +1456,7 @@ namespace Ryujinx.UI.App.Common
if (addedNewDlc)
{
var gameDlcs = it.Items.Where(dlc => dlc.Dlc.TitleIdBase == application.IdBase).ToList();
DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, application.IdBase,
DownloadableContentsHelper.SaveDownloadableContentsJson(application.IdBase,
gameDlcs);
}
}
@ -1483,11 +1475,11 @@ namespace Ryujinx.UI.App.Common
TitleUpdatesHelper.LoadTitleUpdatesJson(_virtualFileSystem, application.IdBase);
it.AddOrUpdate(savedUpdates);
var selectedUpdate = savedUpdates.FirstOrOptional(update => update.IsSelected);
var selectedUpdate = savedUpdates.FindFirst(update => update.IsSelected);
if (TryGetTitleUpdatesFromFile(application.Path, out var bundledUpdates))
{
var savedUpdateLookup = savedUpdates.Select(update => update.Item1).ToHashSet();
var savedUpdateLookup = savedUpdates.Select(update => update.Update).ToHashSet();
bool updatesChanged = false;
foreach (var update in bundledUpdates.OrderByDescending(bundled => bundled.Version))
@ -1495,12 +1487,11 @@ namespace Ryujinx.UI.App.Common
if (!savedUpdateLookup.Contains(update))
{
bool shouldSelect = false;
if (!selectedUpdate.HasValue || selectedUpdate.Value.Item1.Version < update.Version)
if (selectedUpdate.Check(su => su.Update?.Version < update.Version))
{
shouldSelect = true;
if (selectedUpdate.HasValue)
_titleUpdates.AddOrUpdate((selectedUpdate.Value.Item1, false));
selectedUpdate = DynamicData.Kernel.Optional<(TitleUpdateModel, bool IsSelected)>.Create((update, true));
_titleUpdates.AddOrUpdate((selectedUpdate.Value.Update, false));
selectedUpdate = (update, true);
}
modifiedVersion = modifiedVersion || shouldSelect;
@ -1513,7 +1504,7 @@ namespace Ryujinx.UI.App.Common
if (updatesChanged)
{
var gameUpdates = it.Items.Where(update => update.TitleUpdate.TitleIdBase == application.IdBase).ToList();
TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, application.IdBase, gameUpdates);
TitleUpdatesHelper.SaveTitleUpdatesJson(application.IdBase, gameUpdates);
}
}
});
@ -1525,14 +1516,14 @@ namespace Ryujinx.UI.App.Common
private void SaveDownloadableContentsForGame(ulong titleIdBase)
{
var dlcs = DownloadableContents.Items.Where(dlc => dlc.Dlc.TitleIdBase == titleIdBase).ToList();
DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, titleIdBase, dlcs);
DownloadableContentsHelper.SaveDownloadableContentsJson(titleIdBase, dlcs);
}
// Save the _currently tracked_ update state for the game
private void SaveTitleUpdatesForGame(ulong titleIdBase)
{
var updates = TitleUpdates.Items.Where(update => update.TitleUpdate.TitleIdBase == titleIdBase).ToList();
TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, titleIdBase, updates);
TitleUpdatesHelper.SaveTitleUpdatesJson(titleIdBase, updates);
}
// ApplicationData isnt live-updating (e.g. when an update gets applied) and so this is meant to trigger a refresh

View File

@ -1,3 +1,4 @@
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Multiplayer;
@ -16,7 +17,7 @@ namespace Ryujinx.UI.Common.Configuration
/// <summary>
/// The current version of the file format
/// </summary>
public const int CurrentVersion = 56;
public const int CurrentVersion = 57;
/// <summary>
/// Version of the configuration file format
@ -191,8 +192,25 @@ namespace Ryujinx.UI.Common.Configuration
/// <summary>
/// Enables or disables Vertical Sync
/// </summary>
/// <remarks>Kept for file format compatibility (to avoid possible failure when parsing configuration on old versions)</remarks>
/// TODO: Remove this when those older versions aren't in use anymore.
public bool EnableVsync { get; set; }
/// <summary>
/// Current VSync mode; 60 (Switch), unbounded ("Vsync off"), or custom
/// </summary>
public VSyncMode VSyncMode { get; set; }
/// <summary>
/// Enables or disables the custom present interval
/// </summary>
public bool EnableCustomVSyncInterval { get; set; }
/// <summary>
/// The custom present interval value
/// </summary>
public int CustomVSyncInterval { get; set; }
/// <summary>
/// Enables or disables Shader cache
/// </summary>

View File

@ -82,7 +82,7 @@ namespace Ryujinx.UI.Common.Configuration
configurationFileFormat.Hotkeys = new KeyboardHotkeys
{
ToggleVsync = Key.F1,
ToggleVSyncMode = Key.F1,
};
configurationFileUpdated = true;
@ -276,7 +276,7 @@ namespace Ryujinx.UI.Common.Configuration
configurationFileFormat.Hotkeys = new KeyboardHotkeys
{
ToggleVsync = Key.F1,
ToggleVSyncMode = Key.F1,
Screenshot = Key.F8,
};
@ -289,7 +289,7 @@ namespace Ryujinx.UI.Common.Configuration
configurationFileFormat.Hotkeys = new KeyboardHotkeys
{
ToggleVsync = Key.F1,
ToggleVSyncMode = Key.F1,
Screenshot = Key.F8,
ShowUI = Key.F4,
};
@ -332,7 +332,7 @@ namespace Ryujinx.UI.Common.Configuration
configurationFileFormat.Hotkeys = new KeyboardHotkeys
{
ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync,
ToggleVSyncMode = configurationFileFormat.Hotkeys.ToggleVSyncMode,
Screenshot = configurationFileFormat.Hotkeys.Screenshot,
ShowUI = configurationFileFormat.Hotkeys.ShowUI,
Pause = Key.F5,
@ -347,7 +347,7 @@ namespace Ryujinx.UI.Common.Configuration
configurationFileFormat.Hotkeys = new KeyboardHotkeys
{
ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync,
ToggleVSyncMode = configurationFileFormat.Hotkeys.ToggleVSyncMode,
Screenshot = configurationFileFormat.Hotkeys.Screenshot,
ShowUI = configurationFileFormat.Hotkeys.ShowUI,
Pause = configurationFileFormat.Hotkeys.Pause,
@ -421,7 +421,7 @@ namespace Ryujinx.UI.Common.Configuration
configurationFileFormat.Hotkeys = new KeyboardHotkeys
{
ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync,
ToggleVSyncMode = configurationFileFormat.Hotkeys.ToggleVSyncMode,
Screenshot = configurationFileFormat.Hotkeys.Screenshot,
ShowUI = configurationFileFormat.Hotkeys.ShowUI,
Pause = configurationFileFormat.Hotkeys.Pause,
@ -448,7 +448,7 @@ namespace Ryujinx.UI.Common.Configuration
configurationFileFormat.Hotkeys = new KeyboardHotkeys
{
ToggleVsync = configurationFileFormat.Hotkeys.ToggleVsync,
ToggleVSyncMode = configurationFileFormat.Hotkeys.ToggleVSyncMode,
Screenshot = configurationFileFormat.Hotkeys.Screenshot,
ShowUI = configurationFileFormat.Hotkeys.ShowUI,
Pause = configurationFileFormat.Hotkeys.Pause,
@ -611,6 +611,33 @@ namespace Ryujinx.UI.Common.Configuration
configurationFileUpdated = true;
}
if (configurationFileFormat.Version < 57)
{
Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 57.");
configurationFileFormat.VSyncMode = VSyncMode.Switch;
configurationFileFormat.EnableCustomVSyncInterval = false;
configurationFileFormat.Hotkeys = new KeyboardHotkeys
{
ToggleVSyncMode = Key.F1,
Screenshot = configurationFileFormat.Hotkeys.Screenshot,
ShowUI = configurationFileFormat.Hotkeys.ShowUI,
Pause = configurationFileFormat.Hotkeys.Pause,
ToggleMute = configurationFileFormat.Hotkeys.ToggleMute,
ResScaleUp = configurationFileFormat.Hotkeys.ResScaleUp,
ResScaleDown = configurationFileFormat.Hotkeys.ResScaleDown,
VolumeUp = configurationFileFormat.Hotkeys.VolumeUp,
VolumeDown = configurationFileFormat.Hotkeys.VolumeDown,
CustomVSyncIntervalIncrement = Key.Unbound,
CustomVSyncIntervalDecrement = Key.Unbound,
};
configurationFileFormat.CustomVSyncInterval = 120;
configurationFileUpdated = true;
}
Logger.EnableFileLog.Value = configurationFileFormat.EnableFileLog;
Graphics.ResScale.Value = configurationFileFormat.ResScale;
Graphics.ResScaleCustom.Value = configurationFileFormat.ResScaleCustom;
@ -646,7 +673,9 @@ namespace Ryujinx.UI.Common.Configuration
ShowTitleBar.Value = configurationFileFormat.ShowTitleBar;
EnableHardwareAcceleration.Value = configurationFileFormat.EnableHardwareAcceleration;
HideCursor.Value = configurationFileFormat.HideCursor;
Graphics.EnableVsync.Value = configurationFileFormat.EnableVsync;
Graphics.VSyncMode.Value = configurationFileFormat.VSyncMode;
Graphics.EnableCustomVSyncInterval.Value = configurationFileFormat.EnableCustomVSyncInterval;
Graphics.CustomVSyncInterval.Value = configurationFileFormat.CustomVSyncInterval;
Graphics.EnableShaderCache.Value = configurationFileFormat.EnableShaderCache;
Graphics.EnableTextureRecompression.Value = configurationFileFormat.EnableTextureRecompression;
Graphics.EnableMacroHLE.Value = configurationFileFormat.EnableMacroHLE;

View File

@ -1,4 +1,4 @@
using ARMeilleure;
using ARMeilleure;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
@ -474,9 +474,19 @@ namespace Ryujinx.UI.Common.Configuration
public ReactiveObject<string> ShadersDumpPath { get; private set; }
/// <summary>
/// Enables or disables Vertical Sync
/// Toggles the present interval mode. Options are Switch (60Hz), Unbounded (previously Vsync off), and Custom, if enabled.
/// </summary>
public ReactiveObject<bool> EnableVsync { get; private set; }
public ReactiveObject<VSyncMode> VSyncMode { get; private set; }
/// <summary>
/// Enables or disables the custom present interval mode.
/// </summary>
public ReactiveObject<bool> EnableCustomVSyncInterval { get; private set; }
/// <summary>
/// Changes the custom present interval.
/// </summary>
public ReactiveObject<int> CustomVSyncInterval { get; private set; }
/// <summary>
/// Enables or disables Shader cache
@ -536,8 +546,12 @@ namespace Ryujinx.UI.Common.Configuration
AspectRatio = new ReactiveObject<AspectRatio>();
AspectRatio.LogChangesToValue(nameof(AspectRatio));
ShadersDumpPath = new ReactiveObject<string>();
EnableVsync = new ReactiveObject<bool>();
EnableVsync.LogChangesToValue(nameof(EnableVsync));
VSyncMode = new ReactiveObject<VSyncMode>();
VSyncMode.LogChangesToValue(nameof(VSyncMode));
EnableCustomVSyncInterval = new ReactiveObject<bool>();
EnableCustomVSyncInterval.LogChangesToValue(nameof(EnableCustomVSyncInterval));
CustomVSyncInterval = new ReactiveObject<int>();
CustomVSyncInterval.LogChangesToValue(nameof(CustomVSyncInterval));
EnableShaderCache = new ReactiveObject<bool>();
EnableShaderCache.LogChangesToValue(nameof(EnableShaderCache));
EnableTextureRecompression = new ReactiveObject<bool>();

View File

@ -1,16 +1,12 @@
using ARMeilleure;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Configuration.Multiplayer;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.Vulkan;
using Ryujinx.HLE;
using Ryujinx.UI.Common.Configuration.System;
using Ryujinx.UI.Common.Configuration.UI;
using System;
using System.Collections.Generic;
namespace Ryujinx.UI.Common.Configuration
{
@ -21,10 +17,10 @@ namespace Ryujinx.UI.Common.Configuration
if (Instance != null)
{
throw new InvalidOperationException("Configuration is already initialized");
}
}
Instance = new ConfigurationState();
}
}
public ConfigurationFileFormat ToFileFormat()
{
@ -64,7 +60,9 @@ namespace Ryujinx.UI.Common.Configuration
ShowTitleBar = ShowTitleBar,
EnableHardwareAcceleration = EnableHardwareAcceleration,
HideCursor = HideCursor,
EnableVsync = Graphics.EnableVsync,
VSyncMode = Graphics.VSyncMode,
EnableCustomVSyncInterval = Graphics.EnableCustomVSyncInterval,
CustomVSyncInterval = Graphics.CustomVSyncInterval,
EnableShaderCache = Graphics.EnableShaderCache,
EnableTextureRecompression = Graphics.EnableTextureRecompression,
EnableMacroHLE = Graphics.EnableMacroHLE,
@ -179,7 +177,9 @@ namespace Ryujinx.UI.Common.Configuration
ShowTitleBar.Value = !OperatingSystem.IsWindows();
EnableHardwareAcceleration.Value = true;
HideCursor.Value = HideCursorMode.OnIdle;
Graphics.EnableVsync.Value = true;
Graphics.VSyncMode.Value = VSyncMode.Switch;
Graphics.CustomVSyncInterval.Value = 120;
Graphics.EnableCustomVSyncInterval.Value = false;
Graphics.EnableShaderCache.Value = true;
Graphics.EnableTextureRecompression.Value = false;
Graphics.EnableMacroHLE.Value = true;
@ -240,7 +240,7 @@ namespace Ryujinx.UI.Common.Configuration
Hid.EnableMouse.Value = false;
Hid.Hotkeys.Value = new KeyboardHotkeys
{
ToggleVsync = Key.F1,
ToggleVSyncMode = Key.F1,
ToggleMute = Key.F2,
Screenshot = Key.F8,
ShowUI = Key.F4,

View File

@ -247,6 +247,7 @@ namespace Ryujinx.UI.Common
"0100dbf01000a000", // Burnout Paradise Remastered
"0100744001588000", // Cars 3: Driven to Win
"0100b41013c82000", // Cruis'n Blast
"01001b300b9be000", // Diablo III: Eternal Collection
"01008c8012920000", // Dying Light Platinum Edition
"010073c01af34000", // LEGO Horizon Adventures
"0100770008dd8000", // Monster Hunter Generations Ultimate

View File

@ -42,7 +42,7 @@ namespace Ryujinx.UI.Common.Helper
}
}
public static void SaveDownloadableContentsJson(VirtualFileSystem vfs, ulong applicationIdBase, List<(DownloadableContentModel, bool IsEnabled)> dlcs)
public static void SaveDownloadableContentsJson(ulong applicationIdBase, List<(DownloadableContentModel, bool IsEnabled)> dlcs)
{
DownloadableContentContainer container = default;
List<DownloadableContentContainer> downloadableContentContainerList = new();

View File

@ -28,7 +28,7 @@ namespace Ryujinx.UI.Common.Helper
{
private static readonly TitleUpdateMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public static List<(TitleUpdateModel, bool IsSelected)> LoadTitleUpdatesJson(VirtualFileSystem vfs, ulong applicationIdBase)
public static List<(TitleUpdateModel Update, bool IsSelected)> LoadTitleUpdatesJson(VirtualFileSystem vfs, ulong applicationIdBase)
{
var titleUpdatesJsonPath = PathToGameUpdatesJson(applicationIdBase);
@ -49,7 +49,7 @@ namespace Ryujinx.UI.Common.Helper
}
}
public static void SaveTitleUpdatesJson(VirtualFileSystem vfs, ulong applicationIdBase, List<(TitleUpdateModel, bool IsSelected)> updates)
public static void SaveTitleUpdatesJson(ulong applicationIdBase, List<(TitleUpdateModel, bool IsSelected)> updates)
{
var titleUpdateWindowData = new TitleUpdateMetadata
{
@ -77,7 +77,7 @@ namespace Ryujinx.UI.Common.Helper
JsonHelper.SerializeToFile(titleUpdatesJsonPath, titleUpdateWindowData, _serializerContext.TitleUpdateMetadata);
}
private static List<(TitleUpdateModel, bool IsSelected)> LoadTitleUpdates(VirtualFileSystem vfs, TitleUpdateMetadata titleUpdateMetadata, ulong applicationIdBase)
private static List<(TitleUpdateModel Update, bool IsSelected)> LoadTitleUpdates(VirtualFileSystem vfs, TitleUpdateMetadata titleUpdateMetadata, ulong applicationIdBase)
{
var result = new List<(TitleUpdateModel, bool IsSelected)>();

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -5,6 +5,7 @@
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>

View File

@ -11,7 +11,7 @@
</ResourceDictionary>
</Application.Resources>
<Application.Styles>
<sty:FluentAvaloniaTheme PreferSystemTheme="False" />
<sty:FluentAvaloniaTheme PreferUserAccentColor="True" PreferSystemTheme="False" />
<StyleInclude Source="/Assets/Styles/Styles.xaml" />
</Application.Styles>
</Application>

View File

@ -3,6 +3,8 @@ using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input;
using Avalonia.Threading;
using LibHac.Common;
using LibHac.Ns;
using LibHac.Tools.FsSystem;
using Ryujinx.Audio.Backends.Dummy;
using Ryujinx.Audio.Backends.OpenAL;
@ -57,6 +59,8 @@ using Key = Ryujinx.Input.Key;
using MouseButton = Ryujinx.Input.MouseButton;
using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
using Size = Avalonia.Size;
using Switch = Ryujinx.HLE.Switch;
using VSyncMode = Ryujinx.Common.Configuration.VSyncMode;
namespace Ryujinx.Ava
{
@ -203,6 +207,9 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter;
ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel;
ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Event += UpdateColorSpacePassthrough;
ConfigurationState.Instance.Graphics.VSyncMode.Event += UpdateVSyncMode;
ConfigurationState.Instance.Graphics.CustomVSyncInterval.Event += UpdateCustomVSyncIntervalValue;
ConfigurationState.Instance.Graphics.EnableCustomVSyncInterval.Event += UpdateCustomVSyncIntervalEnabled;
ConfigurationState.Instance.System.EnableInternetAccess.Event += UpdateEnableInternetAccessState;
ConfigurationState.Instance.Multiplayer.LanInterfaceId.Event += UpdateLanInterfaceIdState;
@ -295,6 +302,66 @@ namespace Ryujinx.Ava
_renderer.Window?.SetColorSpacePassthrough((bool)ConfigurationState.Instance.Graphics.EnableColorSpacePassthrough.Value);
}
public void UpdateVSyncMode(object sender, ReactiveEventArgs<VSyncMode> e)
{
if (Device != null)
{
Device.VSyncMode = e.NewValue;
Device.UpdateVSyncInterval();
}
_renderer.Window?.ChangeVSyncMode((Ryujinx.Graphics.GAL.VSyncMode)e.NewValue);
_viewModel.ShowCustomVSyncIntervalPicker = (e.NewValue == VSyncMode.Custom);
}
public void VSyncModeToggle()
{
VSyncMode oldVSyncMode = Device.VSyncMode;
VSyncMode newVSyncMode = VSyncMode.Switch;
bool customVSyncIntervalEnabled = ConfigurationState.Instance.Graphics.EnableCustomVSyncInterval.Value;
switch (oldVSyncMode)
{
case VSyncMode.Switch:
newVSyncMode = VSyncMode.Unbounded;
break;
case VSyncMode.Unbounded:
if (customVSyncIntervalEnabled)
{
newVSyncMode = VSyncMode.Custom;
}
else
{
newVSyncMode = VSyncMode.Switch;
}
break;
case VSyncMode.Custom:
newVSyncMode = VSyncMode.Switch;
break;
}
UpdateVSyncMode(this, new ReactiveEventArgs<VSyncMode>(oldVSyncMode, newVSyncMode));
}
private void UpdateCustomVSyncIntervalValue(object sender, ReactiveEventArgs<int> e)
{
if (Device != null)
{
Device.TargetVSyncInterval = e.NewValue;
Device.UpdateVSyncInterval();
}
}
private void UpdateCustomVSyncIntervalEnabled(object sender, ReactiveEventArgs<bool> e)
{
if (Device != null)
{
Device.CustomVSyncIntervalEnabled = e.NewValue;
Device.UpdateVSyncInterval();
}
}
private void ShowCursor()
{
Dispatcher.UIThread.Post(() =>
@ -505,12 +572,6 @@ namespace Ryujinx.Ava
Device.Configuration.MultiplayerDisableP2p = e.NewValue;
}
public void ToggleVSync()
{
Device.EnableDeviceVsync = !Device.EnableDeviceVsync;
_renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync);
}
public void Stop()
{
_isActive = false;
@ -611,7 +672,7 @@ namespace Ryujinx.Ava
_cursorState = CursorStates.ForceChangeCursor;
}
public async Task<bool> LoadGuestApplication()
public async Task<bool> LoadGuestApplication(BlitStruct<ApplicationControlProperty>? customNacpData = null)
{
InitializeSwitchInstance();
MainWindow.UpdateGraphicsConfig();
@ -681,7 +742,7 @@ namespace Ryujinx.Ava
{
Logger.Info?.Print(LogClass.Application, "Loading as Firmware Title (NCA).");
if (!Device.LoadNca(ApplicationPath))
if (!Device.LoadNca(ApplicationPath, customNacpData))
{
Device.Dispose();
@ -864,7 +925,7 @@ namespace Ryujinx.Ava
_viewModel.UiHandler,
(SystemLanguage)ConfigurationState.Instance.System.Language.Value,
(RegionCode)ConfigurationState.Instance.System.Region.Value,
ConfigurationState.Instance.Graphics.EnableVsync,
ConfigurationState.Instance.Graphics.VSyncMode,
ConfigurationState.Instance.System.EnableDockedMode,
ConfigurationState.Instance.System.EnablePtc,
ConfigurationState.Instance.System.EnableInternetAccess,
@ -881,7 +942,8 @@ namespace Ryujinx.Ava
ConfigurationState.Instance.Multiplayer.Mode,
ConfigurationState.Instance.Multiplayer.DisableP2p,
ConfigurationState.Instance.Multiplayer.LdnPassphrase,
ConfigurationState.Instance.Multiplayer.LdnServer));
ConfigurationState.Instance.Multiplayer.LdnServer,
ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value));
}
private static IHardwareDeviceDriver InitializeAudio()
@ -1002,7 +1064,7 @@ namespace Ryujinx.Ava
Device.Gpu.SetGpuThread();
Device.Gpu.InitializeShaderCache(_gpuCancellationTokenSource.Token);
_renderer.Window.ChangeVSyncMode(Device.EnableDeviceVsync);
_renderer.Window.ChangeVSyncMode((Ryujinx.Graphics.GAL.VSyncMode)Device.VSyncMode);
while (_isActive)
{
@ -1063,6 +1125,7 @@ namespace Ryujinx.Ava
{
// Run a status update only when a frame is to be drawn. This prevents from updating the ui and wasting a render when no frame is queued.
string dockedMode = ConfigurationState.Instance.System.EnableDockedMode ? LocaleManager.Instance[LocaleKeys.Docked] : LocaleManager.Instance[LocaleKeys.Handheld];
string vSyncMode = Device.VSyncMode.ToString();
UpdateShaderCount();
@ -1072,11 +1135,11 @@ namespace Ryujinx.Ava
}
StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
Device.EnableDeviceVsync,
vSyncMode,
LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%",
dockedMode,
ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
LocaleManager.Instance[LocaleKeys.Game] + $": {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
$"{Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
$"FIFO: {Device.Statistics.GetFifoPercent():00.00} %",
_displayCount));
}
@ -1175,8 +1238,16 @@ namespace Ryujinx.Ava
{
switch (currentHotkeyState)
{
case KeyboardHotkeyState.ToggleVSync:
ToggleVSync();
case KeyboardHotkeyState.ToggleVSyncMode:
VSyncModeToggle();
break;
case KeyboardHotkeyState.CustomVSyncIntervalDecrement:
Device.DecrementCustomVSyncInterval();
_viewModel.CustomVSyncInterval -= 1;
break;
case KeyboardHotkeyState.CustomVSyncIntervalIncrement:
Device.IncrementCustomVSyncInterval();
_viewModel.CustomVSyncInterval += 1;
break;
case KeyboardHotkeyState.Screenshot:
ScreenshotRequested = true;
@ -1263,9 +1334,9 @@ namespace Ryujinx.Ava
{
KeyboardHotkeyState state = KeyboardHotkeyState.None;
if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVsync))
if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.ToggleVSyncMode))
{
state = KeyboardHotkeyState.ToggleVSync;
state = KeyboardHotkeyState.ToggleVSyncMode;
}
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.Screenshot))
{
@ -1299,6 +1370,14 @@ namespace Ryujinx.Ava
{
state = KeyboardHotkeyState.VolumeDown;
}
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomVSyncIntervalIncrement))
{
state = KeyboardHotkeyState.CustomVSyncIntervalIncrement;
}
else if (_keyboardInterface.IsPressed((Key)ConfigurationState.Instance.Hid.Hotkeys.Value.CustomVSyncIntervalDecrement))
{
state = KeyboardHotkeyState.CustomVSyncIntervalDecrement;
}
return state;
}

View File

@ -1,6 +1,7 @@
{
"Language": "اَلْعَرَبِيَّةُ",
"MenuBarFileOpenApplet": "فتح التطبيق المصغر",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "‫افتح تطبيق تحرير Mii في الوضع المستقل",
"SettingsTabInputDirectMouseAccess": "الوصول المباشر للفأرة",
"SettingsTabSystemMemoryManagerMode": "وضع إدارة الذاكرة:",
@ -30,6 +31,9 @@
"MenuBarToolsInstallFirmware": "تثبيت البرنامج الثابت",
"MenuBarFileToolsInstallFirmwareFromFile": "تثبيت برنامج ثابت من XCI أو ZIP",
"MenuBarFileToolsInstallFirmwareFromDirectory": "تثبيت برنامج ثابت من مجلد",
"MenuBarToolsInstallKeys": "Install Keys",
"MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
"MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
"MenuBarToolsManageFileTypes": "إدارة أنواع الملفات",
"MenuBarToolsInstallFileTypes": "تثبيت أنواع الملفات",
"MenuBarToolsUninstallFileTypes": "إزالة أنواع الملفات",
@ -505,6 +509,13 @@
"DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\nهل تريد المتابعة؟",
"DialogFirmwareInstallerFirmwareInstallWaitMessage": "تثبيت البرنامج الثابت...",
"DialogFirmwareInstallerFirmwareInstallSuccessMessage": "تم تثبيت إصدار النظام {0} بنجاح.",
"DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
"DialogKeysInstallerKeysInstallTitle": "Install Keys",
"DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
"DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
"DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
"DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
"DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
"DialogUserProfileDeletionWarningMessage": "لن تكون هناك ملفات الشخصية أخرى لفتحها إذا تم حذف الملف الشخصي المحدد",
"DialogUserProfileDeletionConfirmMessage": "هل تريد حذف الملف الشخصي المحدد",
"DialogUserProfileUnsavedChangesTitle": "تحذير - التغييرات غير محفوظة",
@ -691,6 +702,9 @@
"Never": "مطلقا",
"SwkbdMinCharacters": "يجب أن يبلغ طوله {0} حرفا على الأقل",
"SwkbdMinRangeCharacters": "يجب أن يتكون من {0}-{1} حرفا",
"CabinetTitle": "Cabinet Dialog",
"CabinetDialog": "Enter your Amiibo's new name",
"CabinetScanDialog": "Please scan your Amiibo now.",
"SoftwareKeyboard": "لوحة المفاتيح البرمجية",
"SoftwareKeyboardModeNumeric": "يجب أن يكون 0-9 أو '.' فقط",
"SoftwareKeyboardModeAlphabet": "يجب أن تكون الأحرف غير CJK فقط",
@ -704,7 +718,6 @@
"UpdaterAddingFiles": "إضافة ملفات جديدة...",
"UpdaterExtracting": "استخراج التحديث...",
"UpdaterDownloading": "تحميل التحديث...",
"Game": "لعبة",
"Docked": "تركيب بالمنصة",
"Handheld": "محمول",
"ConnectionError": "خطأ في الاتصال",
@ -775,7 +788,7 @@
"CheatWindowHeading": "الغش متوفر لـ {0} [{1}]",
"BuildId": "معرف البناء:",
"DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.",
"DlcWindowHeading": "المحتويات القابلة للتنزيل {0}",
"DlcWindowHeading": "{0} DLC(s) available",
"DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added",
"AutoloadDlcAddedMessage": "{0} new downloadable content(s) added",
"AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed",

View File

@ -1,6 +1,7 @@
{
"Language": "Deutsch",
"MenuBarFileOpenApplet": "Öffne Anwendung",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "Öffnet das Mii-Editor-Applet im Standalone-Modus",
"SettingsTabInputDirectMouseAccess": "Direkter Mauszugriff",
"SettingsTabSystemMemoryManagerMode": "Speichermanagermodus:",
@ -30,6 +31,9 @@
"MenuBarToolsInstallFirmware": "Firmware installieren",
"MenuBarFileToolsInstallFirmwareFromFile": "Firmware von einer XCI- oder einer ZIP-Datei installieren",
"MenuBarFileToolsInstallFirmwareFromDirectory": "Firmware aus einem Verzeichnis installieren",
"MenuBarToolsInstallKeys": "Install Keys",
"MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
"MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
"MenuBarToolsManageFileTypes": "Dateitypen verwalten",
"MenuBarToolsInstallFileTypes": "Dateitypen installieren",
"MenuBarToolsUninstallFileTypes": "Dateitypen deinstallieren",
@ -505,6 +509,13 @@
"DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nMöchtest du fortfahren?",
"DialogFirmwareInstallerFirmwareInstallWaitMessage": "Firmware wird installiert...",
"DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Systemversion {0} wurde erfolgreich installiert.",
"DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
"DialogKeysInstallerKeysInstallTitle": "Install Keys",
"DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
"DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
"DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
"DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
"DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
"DialogUserProfileDeletionWarningMessage": "Es können keine anderen Profile geöffnet werden, wenn das ausgewählte Profil gelöscht wird.",
"DialogUserProfileDeletionConfirmMessage": "Möchtest du das ausgewählte Profil löschen?",
"DialogUserProfileUnsavedChangesTitle": "Warnung - Nicht gespeicherte Änderungen",
@ -691,6 +702,9 @@
"Never": "Niemals",
"SwkbdMinCharacters": "Muss mindestens {0} Zeichen lang sein",
"SwkbdMinRangeCharacters": "Muss {0}-{1} Zeichen lang sein",
"CabinetTitle": "Cabinet Dialog",
"CabinetDialog": "Enter your Amiibo's new name",
"CabinetScanDialog": "Please scan your Amiibo now.",
"SoftwareKeyboard": "Software-Tastatur",
"SoftwareKeyboardModeNumeric": "Darf nur 0-9 oder \".\" sein",
"SoftwareKeyboardModeAlphabet": "Keine CJK-Zeichen",
@ -704,7 +718,6 @@
"UpdaterAddingFiles": "Neue Dateien hinzufügen...",
"UpdaterExtracting": "Update extrahieren...",
"UpdaterDownloading": "Update herunterladen...",
"Game": "Spiel",
"Docked": "Docked",
"Handheld": "Handheld",
"ConnectionError": "Verbindungsfehler.",
@ -775,7 +788,7 @@
"CheatWindowHeading": "Cheats verfügbar für {0} [{1}]",
"DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.",
"BuildId": "BuildId:",
"DlcWindowHeading": "DLC verfügbar für {0} [{1}]",
"DlcWindowHeading": "{0} DLC(s) available",
"DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added",
"AutoloadDlcAddedMessage": "{0} new downloadable content(s) added",
"AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed",

View File

@ -1,6 +1,7 @@
{
"Language": "Ελληνικά",
"MenuBarFileOpenApplet": "Άνοιγμα Applet",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "Άνοιγμα του Mii Editor Applet σε Αυτόνομη λειτουργία",
"SettingsTabInputDirectMouseAccess": "Άμεση Πρόσβαση Ποντικιού",
"SettingsTabSystemMemoryManagerMode": "Λειτουργία Διαχείρισης Μνήμης:",
@ -30,6 +31,9 @@
"MenuBarToolsInstallFirmware": "Εγκατάσταση Firmware",
"MenuBarFileToolsInstallFirmwareFromFile": "Εγκατάσταση Firmware από XCI ή ZIP",
"MenuBarFileToolsInstallFirmwareFromDirectory": "Εγκατάσταση Firmware από τοποθεσία",
"MenuBarToolsInstallKeys": "Install Keys",
"MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
"MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
"MenuBarToolsManageFileTypes": "Διαχείριση τύπων αρχείων",
"MenuBarToolsInstallFileTypes": "Εγκαταστήσετε τύπους αρχείων.",
"MenuBarToolsUninstallFileTypes": "Απεγκαταστήσετε τύπους αρχείων",
@ -505,6 +509,13 @@
"DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nΘέλετε να συνεχίσετε;",
"DialogFirmwareInstallerFirmwareInstallWaitMessage": "Εγκατάσταση Firmware...",
"DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Η έκδοση συστήματος {0} εγκαταστάθηκε με επιτυχία.",
"DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
"DialogKeysInstallerKeysInstallTitle": "Install Keys",
"DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
"DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
"DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
"DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
"DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
"DialogUserProfileDeletionWarningMessage": "Δεν θα υπάρχουν άλλα προφίλ εάν διαγραφεί το επιλεγμένο",
"DialogUserProfileDeletionConfirmMessage": "Θέλετε να διαγράψετε το επιλεγμένο προφίλ",
"DialogUserProfileUnsavedChangesTitle": "Προσοχή - Μην Αποθηκευμένες Αλλαγές.",
@ -691,6 +702,9 @@
"Never": "Ποτέ",
"SwkbdMinCharacters": "Πρέπει να έχει μήκος τουλάχιστον {0} χαρακτήρες",
"SwkbdMinRangeCharacters": "Πρέπει να έχει μήκος {0}-{1} χαρακτήρες",
"CabinetTitle": "Cabinet Dialog",
"CabinetDialog": "Enter your Amiibo's new name",
"CabinetScanDialog": "Please scan your Amiibo now.",
"SoftwareKeyboard": "Εικονικό Πληκτρολόγιο",
"SoftwareKeyboardModeNumeric": "Πρέπει να είναι 0-9 ή '.' μόνο",
"SoftwareKeyboardModeAlphabet": "Πρέπει να μην είναι μόνο χαρακτήρες CJK",
@ -704,7 +718,6 @@
"UpdaterAddingFiles": "Προσθήκη Νέων Αρχείων...",
"UpdaterExtracting": "Εξαγωγή Ενημέρωσης...",
"UpdaterDownloading": "Λήψη Ενημέρωσης...",
"Game": "Παιχνίδι",
"Docked": "Προσκολλημένο",
"Handheld": "Χειροκίνητο",
"ConnectionError": "Σφάλμα Σύνδεσης.",
@ -775,7 +788,7 @@
"CheatWindowHeading": "Διαθέσιμα Cheats για {0} [{1}]",
"BuildId": "BuildId:",
"DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.",
"DlcWindowHeading": "{0} Downloadable Content(s) available for {1} ({2})",
"DlcWindowHeading": "{0} DLC(s) available",
"DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added",
"AutoloadDlcAddedMessage": "{0} new downloadable content(s) added",
"AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed",

View File

@ -1,6 +1,7 @@
{
"Language": "English (US)",
"MenuBarFileOpenApplet": "Open Applet",
"MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "Open Mii Editor Applet in Standalone mode",
"SettingsTabInputDirectMouseAccess": "Direct Mouse Access",
"SettingsTabSystemMemoryManagerMode": "Memory Manager Mode:",
@ -30,6 +31,9 @@
"MenuBarToolsInstallFirmware": "Install Firmware",
"MenuBarFileToolsInstallFirmwareFromFile": "Install a firmware from XCI or ZIP",
"MenuBarFileToolsInstallFirmwareFromDirectory": "Install a firmware from a directory",
"MenuBarToolsInstallKeys": "Install Keys",
"MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP",
"MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory",
"MenuBarToolsManageFileTypes": "Manage file types",
"MenuBarToolsInstallFileTypes": "Install file types",
"MenuBarToolsUninstallFileTypes": "Uninstall file types",
@ -142,9 +146,20 @@
"SettingsTabSystemSystemLanguageLatinAmericanSpanish": "Latin American Spanish",
"SettingsTabSystemSystemLanguageSimplifiedChinese": "Simplified Chinese",
"SettingsTabSystemSystemLanguageTraditionalChinese": "Traditional Chinese",
"SettingsTabSystemSystemTimeZone": "System TimeZone:",
"SettingsTabSystemSystemTimeZone": "System Time Zone:",
"SettingsTabSystemSystemTime": "System Time:",
"SettingsTabSystemEnableVsync": "VSync",
"SettingsTabSystemVSyncMode": "VSync:",
"SettingsTabSystemEnableCustomVSyncInterval": "Enable custom refresh rate (Experimental)",
"SettingsTabSystemVSyncModeSwitch": "Switch",
"SettingsTabSystemVSyncModeUnbounded": "Unbounded",
"SettingsTabSystemVSyncModeCustom": "Custom Refresh Rate",
"SettingsTabSystemVSyncModeTooltip": "Emulated Vertical Sync. 'Switch' emulates the Switch's refresh rate of 60Hz. 'Unbounded' is an unbounded refresh rate.",
"SettingsTabSystemVSyncModeTooltipCustom": "Emulated Vertical Sync. 'Switch' emulates the Switch's refresh rate of 60Hz. 'Unbounded' is an unbounded refresh rate. 'Custom' emulates the specified custom refresh rate.",
"SettingsTabSystemEnableCustomVSyncIntervalTooltip": "Allows the user to specify an emulated refresh rate. In some titles, this may speed up or slow down the rate of gameplay logic. In other titles, it may allow for capping FPS at some multiple of the refresh rate, or lead to unpredictable behavior. This is an experimental feature, with no guarantees for how gameplay will be affected. \n\nLeave OFF if unsure.",
"SettingsTabSystemCustomVSyncIntervalValueTooltip": "The custom refresh rate target value.",
"SettingsTabSystemCustomVSyncIntervalSliderTooltip": "The custom refresh rate, as a percentage of the normal Switch refresh rate.",
"SettingsTabSystemCustomVSyncIntervalPercentage": "Custom Refresh Rate %:",
"SettingsTabSystemCustomVSyncIntervalValue": "Custom Refresh Rate Value:",
"SettingsTabSystemEnablePptc": "PPTC (Profiled Persistent Translation Cache)",
"SettingsTabSystemEnableLowPowerPptc": "Low-power PPTC cache",
"SettingsTabSystemEnableFsIntegrityChecks": "FS Integrity Checks",
@ -153,6 +168,7 @@
"SettingsTabSystemAudioBackendOpenAL": "OpenAL",
"SettingsTabSystemAudioBackendSoundIO": "SoundIO",
"SettingsTabSystemAudioBackendSDL2": "SDL2",
"SettingsTabSystemCustomVSyncInterval": "Interval",
"SettingsTabSystemHacks": "Hacks",
"SettingsTabSystemHacksNote": "May cause instability",
"SettingsTabSystemDramSize": "DRAM size:",
@ -505,6 +521,13 @@
"DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\nDo you want to continue?",
"DialogFirmwareInstallerFirmwareInstallWaitMessage": "Installing firmware...",
"DialogFirmwareInstallerFirmwareInstallSuccessMessage": "System version {0} successfully installed.",
"DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}",
"DialogKeysInstallerKeysInstallTitle": "Install Keys",
"DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.",
"DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.",
"DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?",
"DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...",
"DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.",
"DialogUserProfileDeletionWarningMessage": "There would be no other profiles to be opened if selected profile is deleted",
"DialogUserProfileDeletionConfirmMessage": "Do you want to delete the selected profile",
"DialogUserProfileUnsavedChangesTitle": "Warning - Unsaved Changes",
@ -691,6 +714,9 @@
"Never": "Never",
"SwkbdMinCharacters": "Must be at least {0} characters long",
"SwkbdMinRangeCharacters": "Must be {0}-{1} characters long",
"CabinetTitle": "Cabinet Dialog",
"CabinetDialog": "Enter your Amiibo's new name",
"CabinetScanDialog": "Please scan your Amiibo now.",
"SoftwareKeyboard": "Software Keyboard",
"SoftwareKeyboardModeNumeric": "Must be 0-9 or '.' only",
"SoftwareKeyboardModeAlphabet": "Must be non CJK-characters only",
@ -704,7 +730,6 @@
"UpdaterAddingFiles": "Adding New Files...",
"UpdaterExtracting": "Extracting Update...",
"UpdaterDownloading": "Downloading Update...",
"Game": "Game",
"Docked": "Docked",
"Handheld": "Handheld",
"ConnectionError": "Connection Error.",
@ -720,11 +745,13 @@
"RyujinxUpdater": "Ryujinx Updater",
"SettingsTabHotkeys": "Keyboard Hotkeys",
"SettingsTabHotkeysHotkeys": "Keyboard Hotkeys",
"SettingsTabHotkeysToggleVsyncHotkey": "Toggle VSync:",
"SettingsTabHotkeysToggleVSyncModeHotkey": "Toggle VSync mode:",
"SettingsTabHotkeysScreenshotHotkey": "Screenshot:",
"SettingsTabHotkeysShowUiHotkey": "Show UI:",
"SettingsTabHotkeysPauseHotkey": "Pause:",
"SettingsTabHotkeysToggleMuteHotkey": "Mute:",
"SettingsTabHotkeysIncrementCustomVSyncIntervalHotkey": "Raise custom refresh rate",
"SettingsTabHotkeysDecrementCustomVSyncIntervalHotkey": "Lower custom refresh rate",
"ControllerMotionTitle": "Motion Control Settings",
"ControllerRumbleTitle": "Rumble Settings",
"SettingsSelectThemeFileDialogTitle": "Select Theme File",
@ -775,7 +802,7 @@
"CheatWindowHeading": "Cheats Available for {0} [{1}]",
"BuildId": "BuildId:",
"DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.",
"DlcWindowHeading": "{0} Downloadable Content(s) available for {1} ({2})",
"DlcWindowHeading": "{0} DLC(s) available",
"DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added",
"AutoloadDlcAddedMessage": "{0} new downloadable content(s) added",
"AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed",

View File

@ -1,6 +1,7 @@
{
"Language": "Español (ES)",
"MenuBarFileOpenApplet": "Abrir applet",
"MenuBarFileOpenAppletOpenMiiApplet": "Applet Editor Mii",
"MenuBarFileOpenAppletOpenMiiAppletToolTip": "Abre el editor de Mii en modo autónomo",
"SettingsTabInputDirectMouseAccess": "Acceso directo al ratón",
"SettingsTabSystemMemoryManagerMode": "Modo del administrador de memoria:",
@ -30,10 +31,13 @@
"MenuBarToolsInstallFirmware": "Instalar firmware",
"MenuBarFileToolsInstallFirmwareFromFile": "Instalar firmware desde un archivo XCI o ZIP",
"MenuBarFileToolsInstallFirmwareFromDirectory": "Instalar firmware desde una carpeta",
"MenuBarToolsInstallKeys": "Install Keys",
"MenuBarFileToolsInstallKeysFromFile": "Instalar keys de KEYS o ZIP",
"MenuBarFileToolsInstallKeysFromFolder": "Instalar keys de un directorio",
"MenuBarToolsManageFileTypes": "Administrar tipos de archivo",
"MenuBarToolsInstallFileTypes": "Instalar tipos de archivo",
"MenuBarToolsUninstallFileTypes": "Desinstalar tipos de archivo",
"MenuBarToolsXCITrimmer": "Trim XCI Files",
"MenuBarToolsXCITrimmer": "Recortar archivos XCI",
"MenuBarView": "_View",
"MenuBarViewWindow": "Tamaño Ventana",
"MenuBarViewWindow720": "720p",
@ -85,11 +89,11 @@
"GameListContextMenuOpenModsDirectoryToolTip": "Abre el directorio que contiene los Mods de la Aplicación.",
"GameListContextMenuOpenSdModsDirectory": "Abrir Directorio de Mods de Atmosphere\n\n\n\n\n\n",
"GameListContextMenuOpenSdModsDirectoryToolTip": "Abre el directorio alternativo de la tarjeta SD de Atmosphere que contiene los Mods de la Aplicación. Útil para los mods que están empaquetados para el hardware real.",
"GameListContextMenuTrimXCI": "Check and Trim XCI File",
"GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space",
"GameListContextMenuTrimXCI": "Verificar y recortar archivo XCI",
"GameListContextMenuTrimXCIToolTip": "Verificar y recortar archivo XCI para ahorrar espacio en disco",
"StatusBarGamesLoaded": "{0}/{1} juegos cargados",
"StatusBarSystemVersion": "Versión del sistema: {0}",
"StatusBarXCIFileTrimming": "Trimming XCI File '{0}'",
"StatusBarXCIFileTrimming": "Recortando el siguiente archivo XCI: '{0}'",
"LinuxVmMaxMapCountDialogTitle": "Límite inferior para mapeos de memoria detectado",
"LinuxVmMaxMapCountDialogTextPrimary": "¿Quieres aumentar el valor de vm.max_map_count a {0}?",
"LinuxVmMaxMapCountDialogTextSecondary": "Algunos juegos podrían intentar crear más mapeos de memoria de los permitidos. Ryujinx se bloqueará tan pronto como se supere este límite.",
@ -476,7 +480,7 @@
"DialogUninstallFileTypesSuccessMessage": "¡Tipos de archivos desinstalados con éxito!",
"DialogUninstallFileTypesErrorMessage": "No se pudo desinstalar los tipos de archivo.",
"DialogOpenSettingsWindowLabel": "Abrir ventana de opciones",
"DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window",
"DialogOpenXCITrimmerWindowLabel": "Ventana recortador XCI",
"DialogControllerAppletTitle": "Applet de mandos",
"DialogMessageDialogErrorExceptionMessage": "Error al mostrar cuadro de diálogo: {0}",
"DialogSoftwareKeyboardErrorExceptionMessage": "Error al mostrar teclado de software: {0}",
@ -505,6 +509,13 @@
"DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\n¿Continuar?",
"DialogFirmwareInstallerFirmwareInstallWaitMessage": "Instalando firmware...",
"DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Versión de sistema {0} instalada con éxito.",
"DialogKeysInstallerKeysNotFoundErrorMessage": "Se halló un archivo Keys inválido en {0}",
"DialogKeysInstallerKeysInstallTitle": "Instalar Keys",
"DialogKeysInstallerKeysInstallMessage": "Un nuevo archivo Keys será instalado.",
"DialogKeysInstallerKeysInstallSubMessage": "\n\nEsto puede reemplazar algunas de las Keys actualmente instaladas.",
"DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDeseas continuar?",
"DialogKeysInstallerKeysInstallWaitMessage": "Instalando Keys...",
"DialogKeysInstallerKeysInstallSuccessMessage": "Nuevo archivo Keys instalado con éxito.",
"DialogUserProfileDeletionWarningMessage": "Si eliminas el perfil seleccionado no quedará ningún otro perfil",
"DialogUserProfileDeletionConfirmMessage": "¿Quieres eliminar el perfil seleccionado?",
"DialogUserProfileUnsavedChangesTitle": "Advertencia - Cambios sin guardar",
@ -677,20 +688,23 @@
"OpenSetupGuideMessage": "Abrir la guía de instalación",
"NoUpdate": "No actualizado",
"TitleUpdateVersionLabel": "Versión {0} - {1}",
"TitleBundledUpdateVersionLabel": "Bundled: Version {0}",
"TitleBundledDlcLabel": "Bundled:",
"TitleXCIStatusPartialLabel": "Partial",
"TitleXCIStatusTrimmableLabel": "Untrimmed",
"TitleXCIStatusUntrimmableLabel": "Trimmed",
"TitleXCIStatusFailedLabel": "(Failed)",
"TitleXCICanSaveLabel": "Save {0:n0} Mb",
"TitleXCISavingLabel": "Saved {0:n0} Mb",
"TitleBundledUpdateVersionLabel": "Incorporado: Versión {0}",
"TitleBundledDlcLabel": "Incorporado:",
"TitleXCIStatusPartialLabel": "Parcial",
"TitleXCIStatusTrimmableLabel": "Sin recortar",
"TitleXCIStatusUntrimmableLabel": "Recortado",
"TitleXCIStatusFailedLabel": "(Fallido)",
"TitleXCICanSaveLabel": "Ahorra {0:n0} Mb",
"TitleXCISavingLabel": "{0:n0} Mb ahorrado(s)",
"RyujinxInfo": "Ryujinx - Info",
"RyujinxConfirm": "Ryujinx - Confirmación",
"FileDialogAllTypes": "Todos los tipos",
"Never": "Nunca",
"SwkbdMinCharacters": "Debe tener al menos {0} caracteres",
"SwkbdMinRangeCharacters": "Debe tener {0}-{1} caracteres",
"CabinetTitle": "Diálogo Gabinete",
"CabinetDialog": "Ingresa el nuevo nombre de tu Amiibo",
"CabinetScanDialog": "Escanea tu Amiibo ahora.",
"SoftwareKeyboard": "Teclado de software",
"SoftwareKeyboardModeNumeric": "Debe ser sólo 0-9 o '.'",
"SoftwareKeyboardModeAlphabet": "Solo deben ser caracteres no CJK",
@ -704,7 +718,6 @@
"UpdaterAddingFiles": "Añadiendo nuevos archivos...",
"UpdaterExtracting": "Extrayendo actualización...",
"UpdaterDownloading": "Descargando actualización...",
"Game": "Juego",
"Docked": "Dock/TV",
"Handheld": "Portátil",
"ConnectionError": "Error de conexión.",
@ -737,44 +750,44 @@
"SelectDlcDialogTitle": "Selecciona archivo(s) de DLC",
"SelectUpdateDialogTitle": "Selecciona archivo(s) de actualización",
"SelectModDialogTitle": "Seleccionar un directorio de Mods",
"TrimXCIFileDialogTitle": "Check and Trim XCI File",
"TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.",
"TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB",
"TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details",
"TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details",
"TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details",
"TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.",
"TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim",
"TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details",
"TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details",
"TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed",
"TrimXCIFileCancelled": "The operation was cancelled",
"TrimXCIFileFileUndertermined": "No operation was performed",
"TrimXCIFileDialogTitle": "Verificar y recortar archivo XCI",
"TrimXCIFileDialogPrimaryText": "Esta función verificará el espacio vacío y después recortará el archivo XCI para ahorrar espacio en disco",
"TrimXCIFileDialogSecondaryText": "Tamaño de archivo actual: {0:n} MB\nTamaño de datos de juego: {1:n} MB\nAhorro de espacio en disco: {2:n} MB",
"TrimXCIFileNoTrimNecessary": "El archivo XCI no necesita ser recortado. Verifica los logs para más detalles.",
"TrimXCIFileNoUntrimPossible": "El recorte del archivo XCI no puede ser deshecho. Verifica los registros para más detalles.",
"TrimXCIFileReadOnlyFileCannotFix": "El archivo XCI es de solo Lectura y no se le puede escribir. Lee el registro para más información.",
"TrimXCIFileFileSizeChanged": "El archivo XCI ha cambiado de tamaño desde que fue escaneado. Verifica que no se esté escribiendo al archivo y vuelve a intentarlo.",
"TrimXCIFileFreeSpaceCheckFailed": "El archivo XCI tiene datos en el área de espacio libre, no es seguro recortar.",
"TrimXCIFileInvalidXCIFile": "El archivo XCI contiene datos inválidos. Lee el registro para más información.",
"TrimXCIFileFileIOWriteError": "El archivo XCI no se puede abrir para escribirlo. Lee el registro para más información.",
"TrimXCIFileFailedPrimaryText": "El recorte del archivo XCI falló",
"TrimXCIFileCancelled": "La operación fue cancelada",
"TrimXCIFileFileUndertermined": "No se realizó ninguna operación",
"UserProfileWindowTitle": "Administrar perfiles de usuario",
"CheatWindowTitle": "Administrar cheats",
"DlcWindowTitle": "Administrar contenido descargable",
"ModWindowTitle": "Administrar Mods para {0} ({1})",
"UpdateWindowTitle": "Administrar actualizaciones",
"XCITrimmerWindowTitle": "XCI File Trimmer",
"XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected",
"XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)",
"XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...",
"XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...",
"XCITrimmerTitleStatusFailed": "Failed",
"XCITrimmerPotentialSavings": "Potential Savings",
"XCITrimmerActualSavings": "Actual Savings",
"XCITrimmerWindowTitle": "Recortador de archivos XCI",
"XCITrimmerTitleStatusCount": "{0} de {1} Título(s) seleccionado(s)",
"XCITrimmerTitleStatusCountWithFilter": "{0} de {1} Título(s) seleccionado(s) ({2} mostrado(s))",
"XCITrimmerTitleStatusTrimming": "Recortando {0} Título(s)...",
"XCITrimmerTitleStatusUntrimming": "Deshaciendo recorte de {0} Título(s)...",
"XCITrimmerTitleStatusFailed": "Fallido",
"XCITrimmerPotentialSavings": "Ahorro potencial",
"XCITrimmerActualSavings": "Ahorro real",
"XCITrimmerSavingsMb": "{0:n0} Mb",
"XCITrimmerSelectDisplayed": "Select Shown",
"XCITrimmerDeselectDisplayed": "Deselect Shown",
"XCITrimmerSortName": "Title",
"XCITrimmerSortSaved": "Space Savings",
"XCITrimmerTrim": "Trim",
"XCITrimmerUntrim": "Untrim",
"XCITrimmerSelectDisplayed": "Seleccionar mostrado(s)",
"XCITrimmerDeselectDisplayed": "Deseleccionar mostrado(s)",
"XCITrimmerSortName": "Título",
"XCITrimmerSortSaved": "Ahorro de espacio",
"XCITrimmerTrim": "Recortar",
"XCITrimmerUntrim": "Deshacer recorte",
"UpdateWindowUpdateAddedMessage": "{0} nueva(s) actualización(es) agregada(s)",
"UpdateWindowBundledContentNotice": "Las actualizaciones agrupadas no pueden ser eliminadas, solamente deshabilitadas.",
"CheatWindowHeading": "Cheats disponibles para {0} [{1}]",
"BuildId": "Id de compilación:",
"DlcWindowHeading": "Contenido descargable disponible para {0} [{1}]",
"DlcWindowHeading": "{0} DLC(s) available",
"DlcWindowDlcAddedMessage": "Se agregaron {0} nuevo(s) contenido(s) descargable(s)",
"AutoloadDlcAddedMessage": "Se agregaron {0} nuevo(s) contenido(s) descargable(s)",
"AutoloadDlcRemovedMessage": "Se eliminaron {0} contenido(s) descargable(s) faltantes",
@ -782,7 +795,7 @@
"AutoloadUpdateRemovedMessage": "Se eliminaron {0} actualización(es) faltantes",
"ModWindowHeading": "{0} Mod(s)",
"UserProfilesEditProfile": "Editar selección",
"Continue": "Continue",
"Continue": "Continuar",
"Cancel": "Cancelar",
"Save": "Guardar",
"Discard": "Descartar",

Some files were not shown because too many files have changed in this diff Show More