Compare commits

...

69 Commits

Author SHA1 Message Date
IvonWei
6e8a283119 Merge 9356b68f26 into 4868fface8 2025-01-18 23:35:36 +02:00
Evan Husted
4868fface8 UI: Intel Mac warning
Upon launch, shows a warning about using an Intel Mac. This will only show once every boot. You can only turn it off by getting a better system.
2025-01-18 15:33:05 -06:00
Evan Husted
6fca4492d0 misc: chore: Remove status update event stuff in Headless 2025-01-18 15:15:08 -06:00
Evan Husted
ade2f256e0 misc: chore: remove duplicate graphics debug levels in headless windows 2025-01-18 11:19:38 -06:00
Evan Husted
580b150c9a Revert "infra: Conditionally compile Metal & OpenGL depending on if the target RuntimeIdentifier is mac"
This reverts commit 2f93a0f706.
2025-01-18 10:57:02 -06:00
Evan Husted
e6bad52945 Revert "Only selectively compile Metal & fix some compilation issues"
This reverts commit beda3206e0.
2025-01-18 10:56:58 -06:00
Evan Husted
beda3206e0 Only selectively compile Metal & fix some compilation issues 2025-01-18 10:52:32 -06:00
Evan Husted
2f93a0f706 infra: Conditionally compile Metal & OpenGL depending on if the target RuntimeIdentifier is mac 2025-01-18 10:38:29 -06:00
Evan Husted
80f44d9547 misc: chore: small cleanup 2025-01-18 10:33:57 -06:00
Evan Husted
b08e5db6d8 Headless: Dispose of inputmanager in a catch-all try 2025-01-18 10:30:19 -06:00
Evan Husted
6a291d4116 Headless: Use main UI logo for window icon instead of separate bmp 2025-01-18 10:26:12 -06:00
Evan Husted
6fc827fe67 headless: collapse headless window definition into a "Windows" folder, change GetWindowFlags to an abstract property. 2025-01-18 10:15:24 -06:00
Daniel Nylander
6cd4866d76 Updated sv_SE in locales.json (#513) 2025-01-17 18:04:18 -06:00
WilliamWsyHK
4d7ca5c0f0 Update Chinese translations (#375) 2025-01-17 17:35:34 -06:00
GabCoolGuy
a375faecc1 UI: Fix UpdateWaitWindow.axaml windows being too big on windows (#314) 2025-01-17 14:14:19 -06:00
Evan Husted
1728b0f20c WWE 2K18 is not playable. 2025-01-17 11:37:08 -06:00
LotP1
5aa071c59b remove notice for unusual core counts (#531) 2025-01-17 05:50:42 -06:00
Daenorth
1018c9db8b Update Norwegian Translation (#503)
Norwegian translation updated with the Compatibility list addition
2025-01-16 10:02:33 -06:00
Evan Husted
01ccd18726 UI: Meant to use that method in another place [ci-skip] 2025-01-16 09:52:35 -06:00
Evan Husted
abfbc6f4bc UI: Prevent desynced RPC when toggling it off/on while in-game 2025-01-16 09:52:01 -06:00
Francesco Saltori
6a4bc02d7a Update Italian translation (#489) 2025-01-16 06:38:36 -06:00
Hack茶ん
814c0526d2 Korean translations for compat list (#502) 2025-01-16 04:57:32 -06:00
Evan Husted
a5a4ef38e6 HLE: Stub IHidServer SetGestureOutputRanges (#524)
Lets "Donkey Kong Country Returns HD" get into main gameplay.
2025-01-16 02:39:39 -06:00
Evan Husted
c17e3bfcdf genuinely dont know how that was still there, i thought i got rid of UI.Common 2025-01-15 03:01:17 -06:00
Evan Husted
017f46f318 HLE: misc: throw a more descriptive error when the loaded processes doesn't contain _latestPid (likely missing FW) 2025-01-15 03:01:17 -06:00
Keaton
fd4d801bfd Various NuGet package updates (#203)
Updates the following packages:

**nuget: bump the avalonia group with 7 updates**

* Bump Avalonia, Avalonia.Controls.DataGrid, Avalonia.Desktop,
Avalonia.Diagnostics, and Avalonia.Markup.Xaml.Loader from 11.0.10 to
11.0.13
* Bump Avalonia.Svg and Avalonia.Svg.Skia from 11.0.0.18 to 11.0.0.19

**nuget: bump non-avalonia packages**

* Bump Concentus from 2.2.0 to 2.2.2
* Bump Microsoft.IdentityModel.JsonWebTokens from 8.1.2 to 8.3.0
* Bump Silk.NET.Vulkan group with 3 updates (2.21.0 to 2.22.0)
* Bump SkiaSharp group with 2 updates (2.88.7 to 2.88.9)
2025-01-13 11:15:05 -06:00
madwind
9356b68f26 Add a '*' to label the virtual controller. 2025-01-13 08:41:32 +08:00
IvonWei
14aafebaa6 Merge branch 'Ryubing:master' into master 2025-01-13 08:33:30 +08:00
GabCoolGuy
f1dee50275 infra: Update to LLVM 17 (#519)
This fixes macos builds not building correctly because of a missing
LLVM 14 package.
2025-01-12 14:57:57 -06:00
shinyoyo
c2ae49eb47 Add some missing Simplified Chinese translations (#515) 2025-01-12 12:33:27 -06:00
WilliamWsyHK
47c71966d0 Add compat of Xenoblade 2 JP edition same as global edition (#517) 2025-01-12 12:31:58 -06:00
Evan Husted
f9e8f4bc29 docs: compat: Ori and the Will of the Wisps is now ingame, not playable 2025-01-11 06:10:58 -06:00
Evan Husted
7694c8c046 Do not auto release stable 2025-01-11 04:08:11 -06:00
Evan Husted
0dd789e8a5 misc: chore: remove redundant trimming on CompatibilityEntry.GameName init 2025-01-11 01:26:34 -06:00
Evan Husted
4e0aafd005 docs: compat: Trim redundant/duplicate information to save space 2025-01-11 01:18:10 -06:00
Evan Husted
c5091f499e docs: compat: The House of the Dead: Remake Playable 2025-01-11 00:38:32 -06:00
Evan Husted
41c8fd8194 misc: chore: lol this field was misspelled 2025-01-10 23:23:53 -06:00
Evan Husted
d4a7ee25ea misc: chore: use ObservableProperty on input view models 2025-01-10 23:23:05 -06:00
Evan Husted
3141c560fb misc: chore: remove sender parameter from LdnGameData receieved event 2025-01-10 23:15:55 -06:00
Evan Husted
de341b285b misc: use ObservableProperty on HotkeyConfig fields 2025-01-10 23:15:37 -06:00
Evan Husted
4518666a04 Merge branch 'master' into master 2025-01-10 21:39:41 -06:00
Evan Husted
cc95e80ee9 misc: chore: Move converters into a directory in Helpers. Namespace unchanged 2025-01-10 20:24:53 -06:00
Evan Husted
d75ce52bd4 UI: Show play time in one time unit, maxing out at hours. 2025-01-10 20:23:47 -06:00
Evan Husted
4a4ea557de UI: compat: show last updated date on entry hover 2025-01-10 01:43:34 -06:00
madwind
4399edaa9f Fix typo: SQL_JOYBATTERYUPDATED => SDL_JOYBATTERYUPDATED 2025-01-02 11:50:20 +08:00
Evan Husted
4e77bcb55a Merge branch 'master' into master 2025-01-01 21:19:02 -06:00
IvonWei
3cbd7dc1a1 Merge branch 'Ryubing:master' into master 2024-12-31 19:21:10 +08:00
IvonWei
536f792558 Merge branch 'Ryubing:master' into master 2024-12-30 22:04:08 +08:00
madwind
7a451ab160 fix GetMappedStateSnapshot 2024-12-30 22:01:21 +08:00
IvonWei
99c7c3fb14 Merge branch 'Ryubing:master' into master 2024-12-29 18:37:03 +08:00
Evan Husted
09e7b660f4 Merge branch 'master' into master 2024-12-29 03:42:20 -06:00
madwind
69dfd8c60e fix right Stick 2024-12-29 02:11:31 +08:00
madwind
8e50dd9fa6 fix right JoyCon stick 2024-12-29 01:49:25 +08:00
madwind
68c03051ad For the JoyCon controller, wrap SDL2Gamepad as SDL2JoyCon to use a suitable layout and correct the motion sensing and joystick orientation. 2024-12-29 00:56:03 +08:00
Evan Husted
a837294b11 Merge branch 'master' into master 2024-12-28 06:01:06 -06:00
IvonWei
f1c0cc8076 Merge branch 'Ryubing:master' into master 2024-12-28 09:09:10 +08:00
madwind
6dec7ff8ba fix motionData 2024-12-28 09:07:22 +08:00
madwind
20fdbff964 clean log 2024-12-26 14:47:40 +08:00
IvonWei
e426680cb0 Merge branch 'GreemDev:master' into master 2024-12-26 11:58:50 +08:00
madwind
7863e97cb0 invoke OnGamepadConnected and OnGamepadDisconnected 2024-12-26 11:58:00 +08:00
madwind
c4dea0ee28 add SQL_JOYBATTERYUPDATED , OnJoyBatteryUpdated 2024-12-26 11:54:52 +08:00
IvonWei
e0b6a01e9d Merge branch 'GreemDev:master' into master 2024-12-25 17:00:53 +08:00
madwind
e509ffa716 delay 2000ms before ShowPowerLevel 2024-12-25 16:57:36 +08:00
IvonWei
714c68b548 Merge branch 'GreemDev:master' into master 2024-12-25 10:41:20 +08:00
madwind
fec197d9ec log powerLevel 2024-12-25 10:39:07 +08:00
IvonWei
a4b2feef79 Merge branch 'GreemDev:master' into master 2024-12-23 22:13:47 +08:00
IvonWei
ad7d9d1ce0 Update NpadController.cs
add ?
2024-12-23 18:55:49 +08:00
IvonWei
86f9544910 Update NpadController.cs
back to Debug
2024-12-23 18:54:11 +08:00
madwind
e9ecbd44fc Add a virtual controller to merge Joy-Cons. 2024-12-23 17:57:55 +08:00
60 changed files with 4727 additions and 4237 deletions

View File

@@ -129,11 +129,11 @@ jobs:
with:
global-json-file: global.json
- name: Setup LLVM 14
- name: Setup LLVM 17
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 14
sudo ./llvm.sh 17
- name: Install rcodesign
run: |

View File

@@ -210,11 +210,11 @@ jobs:
with:
global-json-file: global.json
- name: Setup LLVM 15
- name: Setup LLVM 17
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 15
sudo ./llvm.sh 17
- name: Install rcodesign
run: |

View File

@@ -3,16 +3,6 @@ name: Release job
on:
workflow_dispatch:
inputs: {}
push:
branches: [ release ]
paths-ignore:
- '.github/**'
- 'docs/**'
- 'assets/**'
- '*.yml'
- '*.json'
- '*.config'
- '*.md'
concurrency: release
@@ -201,11 +191,11 @@ jobs:
with:
global-json-file: global.json
- name: Setup LLVM 15
- name: Setup LLVM 17
run: |
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
sudo ./llvm.sh 15
sudo ./llvm.sh 17
- name: Install rcodesign
run: |

View File

@@ -3,13 +3,13 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia" Version="11.0.10" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.10" />
<PackageVersion Include="Avalonia.Desktop" Version="11.0.10" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.10" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.10" />
<PackageVersion Include="Avalonia.Svg" Version="11.0.0.18" />
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.18" />
<PackageVersion Include="Avalonia" Version="11.0.13" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.0.13" />
<PackageVersion Include="Avalonia.Desktop" Version="11.0.13" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.0.13" />
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.0.13" />
<PackageVersion Include="Avalonia.Svg" Version="11.0.0.19" />
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.0.0.19" />
<PackageVersion Include="Microsoft.Build.Framework" Version="17.11.4" />
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="17.12.6" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
@@ -18,7 +18,7 @@
<PackageVersion Include="Projektanker.Icons.Avalonia.MaterialDesign" Version="9.4.0"/>
<PackageVersion Include="CommandLineParser" Version="2.9.1" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0"/>
<PackageVersion Include="Concentus" Version="2.2.0" />
<PackageVersion Include="Concentus" Version="2.2.2" />
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageVersion Include="DynamicData" Version="9.0.4" />
<PackageVersion Include="FluentAvaloniaUI" Version="2.0.5" />
@@ -26,7 +26,7 @@
<PackageVersion Include="LibHac" Version="0.19.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.1.2" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.3.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.1" />
<PackageVersion Include="MsgPack.Cli" Version="1.0.1" />
@@ -42,17 +42,17 @@
<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.7.0.1" />
<PackageVersion Include="Gommon" Version="2.7.0.2" />
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
<PackageVersion Include="Sep" Version="0.6.0" />
<PackageVersion Include="shaderc.net" Version="0.1.0" />
<PackageVersion Include="SharpMetal" Version="1.0.0-preview21" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="Silk.NET.Vulkan" Version="2.21.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.21.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.21.0" />
<PackageVersion Include="SkiaSharp" Version="2.88.7" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.7" />
<PackageVersion Include="Silk.NET.Vulkan" Version="2.22.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.22.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.22.0" />
<PackageVersion Include="SkiaSharp" Version="2.88.9" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.9" />
<PackageVersion Include="SPB" Version="0.0.4-build32" />
<PackageVersion Include="System.IO.Hashing" Version="9.0.0" />
<PackageVersion Include="System.Management" Version="9.0.0" />

View File

@@ -19,7 +19,7 @@ if platform.system() == "Darwin":
else:
OTOOL = shutil.which("llvm-otool")
if OTOOL is None:
for llvm_ver in [15, 14, 13]:
for llvm_ver in [17, 16, 15, 14, 13]:
otool_path = shutil.which(f"llvm-otool-{llvm_ver}")
if otool_path is not None:
OTOOL = otool_path

View File

@@ -26,7 +26,7 @@ else:
LIPO = shutil.which("llvm-lipo")
if LIPO is None:
for llvm_ver in [15, 14, 13]:
for llvm_ver in [17, 16, 15, 14, 13]:
lipo_path = shutil.which(f"llvm-lipo-{llvm_ver}")
if lipo_path is not None:
LIPO = lipo_path

View File

@@ -67,11 +67,11 @@ python3 "$BASE_DIR/distribution/macos/construct_universal_dylib.py" "$ARM64_APP_
if ! [ -x "$(command -v lipo)" ];
then
if ! [ -x "$(command -v llvm-lipo-14)" ];
if ! [ -x "$(command -v llvm-lipo-17)" ];
then
LIPO=llvm-lipo
else
LIPO=llvm-lipo-14
LIPO=llvm-lipo-17
fi
else
LIPO=lipo

View File

@@ -62,11 +62,11 @@ python3 "$BASE_DIR/distribution/macos/construct_universal_dylib.py" "$ARM64_OUTP
if ! [ -x "$(command -v lipo)" ];
then
if ! [ -x "$(command -v llvm-lipo-14)" ];
if ! [ -x "$(command -v llvm-lipo-17)" ];
then
LIPO=llvm-lipo
else
LIPO=llvm-lipo-14
LIPO=llvm-lipo-17
fi
else
LIPO=lipo

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
using System;
namespace Ryujinx.Common
{
public class RyujinxException : Exception
{
public RyujinxException(string message) : base(message)
{ }
}
}

View File

@@ -92,7 +92,7 @@ namespace Ryujinx.Graphics.Vulkan
DriverId.MesaDozen => "Dozen",
DriverId.MesaNvk => "NVK",
DriverId.ImaginationOpenSourceMesa => "Imagination (Open)",
DriverId.MesaAgxv => "Honeykrisp",
DriverId.MesaHoneykrisp => "Honeykrisp",
_ => id.ToString(),
};
}

View File

@@ -176,9 +176,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process
AllowedCpuCoresMask = GetMaskFromMinMax(lowestCpuCore, highestCpuCore);
AllowedThreadPriosMask = GetMaskFromMinMax(lowestThreadPrio, highestThreadPrio);
if (isApplication && lowestCpuCore == 0 && highestCpuCore != 2)
Ryujinx.Common.Logging.Logger.Error?.Print(Ryujinx.Common.Logging.LogClass.Application, $"Application requested cores with index range {lowestCpuCore} to {highestCpuCore}! Report this to @LotP on the Ryujinx/Ryubing discord server (discord.gg/ryujinx)!");
else if (isApplication)
if (isApplication)
Ryujinx.Common.Logging.Logger.Info?.Print(Ryujinx.Common.Logging.LogClass.Application, $"Application requested cores with index range {lowestCpuCore} to {highestCpuCore}");
break;

View File

@@ -702,6 +702,18 @@ namespace Ryujinx.HLE.HOS.Services.Hid
return ResultCode.Success;
}
[CommandCmif(92)]
// SetGestureOutputRanges(pid, ushort Unknown0)
public ResultCode SetGestureOutputRanges(ServiceCtx context)
{
ulong pid = context.Request.HandleDesc.PId;
ushort unknown0 = context.RequestData.ReadUInt16();
Logger.Stub?.PrintStub(LogClass.ServiceHid, new { pid, unknown0 });
return ResultCode.Success;
}
[CommandCmif(100)]
// SetSupportedNpadStyleSet(pid, nn::applet::AppletResourceUserId, nn::hid::NpadStyleTag)

View File

@@ -26,7 +26,17 @@ namespace Ryujinx.HLE.Loaders.Processes
private ulong _latestPid;
public ProcessResult ActiveApplication => _processesByPid[_latestPid];
public ProcessResult ActiveApplication
{
get
{
if (!_processesByPid.TryGetValue(_latestPid, out ProcessResult value))
throw new RyujinxException(
$"The HLE Process map did not have a process with ID {_latestPid}. Are you missing firmware?");
return value;
}
}
public ProcessLoader(Switch device)
{

View File

@@ -1,3 +1,4 @@
using Ryujinx.Common.Logging;
using Ryujinx.SDL2.Common;
using System;
using System.Collections.Generic;
@@ -36,6 +37,7 @@ namespace Ryujinx.Input.SDL2
SDL2Driver.Instance.Initialize();
SDL2Driver.Instance.OnJoyStickConnected += HandleJoyStickConnected;
SDL2Driver.Instance.OnJoystickDisconnected += HandleJoyStickDisconnected;
SDL2Driver.Instance.OnJoyBatteryUpdated += HandleJoyBatteryUpdated;
// Add already connected gamepads
int numJoysticks = SDL_NumJoysticks();
@@ -83,19 +85,30 @@ namespace Ryujinx.Input.SDL2
private void HandleJoyStickDisconnected(int joystickInstanceId)
{
bool joyConPairDisconnected = false;
if (!_gamepadsInstanceIdsMapping.Remove(joystickInstanceId, out string id))
return;
lock (_lock)
{
_gamepadsIds.Remove(id);
if (!SDL2JoyConPair.IsCombinable(_gamepadsIds))
{
_gamepadsIds.Remove(SDL2JoyConPair.Id);
joyConPairDisconnected = true;
}
}
OnGamepadDisconnected?.Invoke(id);
if (joyConPairDisconnected)
{
OnGamepadDisconnected?.Invoke(SDL2JoyConPair.Id);
}
}
private void HandleJoyStickConnected(int joystickDeviceId, int joystickInstanceId)
{
bool joyConPairConnected = false;
if (SDL_IsGameController(joystickDeviceId) == SDL_bool.SDL_TRUE)
{
if (_gamepadsInstanceIdsMapping.ContainsKey(joystickInstanceId))
@@ -120,13 +133,29 @@ namespace Ryujinx.Input.SDL2
_gamepadsIds.Insert(joystickDeviceId, id);
else
_gamepadsIds.Add(id);
if (SDL2JoyConPair.IsCombinable(_gamepadsIds))
{
_gamepadsIds.Remove(SDL2JoyConPair.Id);
_gamepadsIds.Add(SDL2JoyConPair.Id);
joyConPairConnected = true;
}
}
OnGamepadConnected?.Invoke(id);
if (joyConPairConnected)
{
OnGamepadConnected?.Invoke(SDL2JoyConPair.Id);
}
}
}
}
private void HandleJoyBatteryUpdated(int joystickDeviceId, SDL_JoystickPowerLevel powerLevel)
{
Logger.Info?.Print(LogClass.Hid,
$"{SDL_GameControllerNameForIndex(joystickDeviceId)} power level: {powerLevel}");
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
@@ -157,6 +186,14 @@ namespace Ryujinx.Input.SDL2
public IGamepad GetGamepad(string id)
{
if (id == SDL2JoyConPair.Id)
{
lock (_lock)
{
return SDL2JoyConPair.GetGamepad(_gamepadsIds);
}
}
int joystickIndex = GetJoystickIndexByGamepadId(id);
if (joystickIndex == -1)
@@ -165,12 +202,16 @@ namespace Ryujinx.Input.SDL2
}
nint gamepadHandle = SDL_GameControllerOpen(joystickIndex);
if (gamepadHandle == nint.Zero)
{
return null;
}
if (SDL_GameControllerName(gamepadHandle).StartsWith(SDL2JoyCon.Prefix))
{
return new SDL2JoyCon(gamepadHandle, id);
}
return new SDL2Gamepad(gamepadHandle, id);
}
}

View File

@@ -0,0 +1,409 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading;
using static SDL2.SDL;
namespace Ryujinx.Input.SDL2
{
internal class SDL2JoyCon : IGamepad
{
private bool HasConfiguration => _configuration != null;
private readonly record struct ButtonMappingEntry(GamepadButtonInputId To, GamepadButtonInputId From)
{
public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not GamepadButtonInputId.Unbound;
}
private StandardControllerInputConfig _configuration;
private readonly Dictionary<GamepadButtonInputId,SDL_GameControllerButton> _leftButtonsDriverMapping = new()
{
{ GamepadButtonInputId.LeftStick , SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSTICK },
{GamepadButtonInputId.DpadUp ,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_Y},
{GamepadButtonInputId.DpadDown ,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_A},
{GamepadButtonInputId.DpadLeft ,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_B},
{GamepadButtonInputId.DpadRight ,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_X},
{GamepadButtonInputId.Minus ,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_START},
{GamepadButtonInputId.LeftShoulder,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE2},
{GamepadButtonInputId.LeftTrigger,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE4},
{GamepadButtonInputId.SingleRightTrigger0,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER},
{GamepadButtonInputId.SingleLeftTrigger0,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSHOULDER},
};
private readonly Dictionary<GamepadButtonInputId,SDL_GameControllerButton> _rightButtonsDriverMapping = new()
{
{GamepadButtonInputId.RightStick,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSTICK},
{GamepadButtonInputId.A,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_B},
{GamepadButtonInputId.B,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_Y},
{GamepadButtonInputId.X,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_A},
{GamepadButtonInputId.Y,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_X},
{GamepadButtonInputId.Plus,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_START},
{GamepadButtonInputId.RightShoulder,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE1},
{GamepadButtonInputId.RightTrigger,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_PADDLE3},
{GamepadButtonInputId.SingleRightTrigger1,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_RIGHTSHOULDER},
{GamepadButtonInputId.SingleLeftTrigger1,SDL_GameControllerButton.SDL_CONTROLLER_BUTTON_LEFTSHOULDER}
};
private readonly Dictionary<GamepadButtonInputId, SDL_GameControllerButton> _buttonsDriverMapping;
private readonly Lock _userMappingLock = new();
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
private readonly StickInputId[] _stickUserMapping = new StickInputId[(int)StickInputId.Count]
{
StickInputId.Unbound, StickInputId.Left, StickInputId.Right,
};
public GamepadFeaturesFlag Features { get; }
private nint _gamepadHandle;
private enum JoyConType
{
Left, Right
}
public const string Prefix = "Nintendo Switch Joy-Con";
public const string LeftName = "Nintendo Switch Joy-Con (L)";
public const string RightName = "Nintendo Switch Joy-Con (R)";
private readonly JoyConType _joyConType;
public SDL2JoyCon(nint gamepadHandle, string driverId)
{
_gamepadHandle = gamepadHandle;
_buttonsUserMapping = new List<ButtonMappingEntry>(10);
Name = SDL_GameControllerName(_gamepadHandle);
Id = driverId;
Features = GetFeaturesFlag();
// Enable motion tracking
if (Features.HasFlag(GamepadFeaturesFlag.Motion))
{
if (SDL_GameControllerSetSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL,
SDL_bool.SDL_TRUE) != 0)
{
Logger.Error?.Print(LogClass.Hid,
$"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_ACCEL}.");
}
if (SDL_GameControllerSetSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO,
SDL_bool.SDL_TRUE) != 0)
{
Logger.Error?.Print(LogClass.Hid,
$"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_GYRO}.");
}
}
switch (Name)
{
case LeftName:
{
_buttonsDriverMapping = _leftButtonsDriverMapping;
_joyConType = JoyConType.Left;
break;
}
case RightName:
{
_buttonsDriverMapping = _rightButtonsDriverMapping;
_joyConType = JoyConType.Right;
break;
}
}
}
private GamepadFeaturesFlag GetFeaturesFlag()
{
GamepadFeaturesFlag result = GamepadFeaturesFlag.None;
if (SDL_GameControllerHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL) == SDL_bool.SDL_TRUE &&
SDL_GameControllerHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO) == SDL_bool.SDL_TRUE)
{
result |= GamepadFeaturesFlag.Motion;
}
int error = SDL_GameControllerRumble(_gamepadHandle, 0, 0, 100);
if (error == 0)
{
result |= GamepadFeaturesFlag.Rumble;
}
return result;
}
public string Id { get; }
public string Name { get; }
public bool IsConnected => SDL_GameControllerGetAttached(_gamepadHandle) == SDL_bool.SDL_TRUE;
protected virtual void Dispose(bool disposing)
{
if (disposing && _gamepadHandle != nint.Zero)
{
SDL_GameControllerClose(_gamepadHandle);
_gamepadHandle = nint.Zero;
}
}
public void Dispose()
{
Dispose(true);
}
public void SetTriggerThreshold(float triggerThreshold)
{
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
if (!Features.HasFlag(GamepadFeaturesFlag.Rumble))
return;
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
if (durationMs == uint.MaxValue)
{
if (SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, SDL_HAPTIC_INFINITY) !=
0)
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller.");
}
else if (durationMs > SDL_HAPTIC_INFINITY)
{
Logger.Error?.Print(LogClass.Hid, $"Unsupported rumble duration {durationMs}");
}
else
{
if (SDL_GameControllerRumble(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs) != 0)
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller.");
}
}
public Vector3 GetMotionData(MotionInputId inputId)
{
SDL_SensorType sensorType = inputId switch
{
MotionInputId.Accelerometer => SDL_SensorType.SDL_SENSOR_ACCEL,
MotionInputId.Gyroscope => SDL_SensorType.SDL_SENSOR_GYRO,
_ => SDL_SensorType.SDL_SENSOR_INVALID
};
if (!Features.HasFlag(GamepadFeaturesFlag.Motion) || sensorType is SDL_SensorType.SDL_SENSOR_INVALID)
return Vector3.Zero;
const int ElementCount = 3;
unsafe
{
float* values = stackalloc float[ElementCount];
int result = SDL_GameControllerGetSensorData(_gamepadHandle, sensorType, (nint)values, ElementCount);
if (result != 0)
return Vector3.Zero;
Vector3 value = _joyConType switch
{
JoyConType.Left => new Vector3(-values[2], values[1], values[0]),
JoyConType.Right => new Vector3(values[2], values[1], -values[0])
};
return inputId switch
{
MotionInputId.Gyroscope => RadToDegree(value),
MotionInputId.Accelerometer => GsToMs2(value),
_ => value
};
}
}
private static Vector3 RadToDegree(Vector3 rad) => rad * (180 / MathF.PI);
private static Vector3 GsToMs2(Vector3 gs) => gs / SDL_STANDARD_GRAVITY;
public void SetConfiguration(InputConfig configuration)
{
lock (_userMappingLock)
{
_configuration = (StandardControllerInputConfig)configuration;
_buttonsUserMapping.Clear();
// First update sticks
_stickUserMapping[(int)StickInputId.Left] = (StickInputId)_configuration.LeftJoyconStick.Joystick;
_stickUserMapping[(int)StickInputId.Right] = (StickInputId)_configuration.RightJoyconStick.Joystick;
switch (_joyConType)
{
case JoyConType.Left:
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (GamepadButtonInputId)_configuration.LeftJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (GamepadButtonInputId)_configuration.LeftJoycon.DpadUp));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (GamepadButtonInputId)_configuration.LeftJoycon.DpadDown));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (GamepadButtonInputId)_configuration.LeftJoycon.DpadLeft));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (GamepadButtonInputId)_configuration.LeftJoycon.DpadRight));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonMinus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonL));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonZl));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (GamepadButtonInputId)_configuration.LeftJoycon.ButtonSl));
break;
case JoyConType.Right:
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (GamepadButtonInputId)_configuration.RightJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (GamepadButtonInputId)_configuration.RightJoycon.ButtonA));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (GamepadButtonInputId)_configuration.RightJoycon.ButtonB));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (GamepadButtonInputId)_configuration.RightJoycon.ButtonX));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (GamepadButtonInputId)_configuration.RightJoycon.ButtonY));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (GamepadButtonInputId)_configuration.RightJoycon.ButtonPlus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (GamepadButtonInputId)_configuration.RightJoycon.ButtonR));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (GamepadButtonInputId)_configuration.RightJoycon.ButtonZr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (GamepadButtonInputId)_configuration.RightJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (GamepadButtonInputId)_configuration.RightJoycon.ButtonSl));
break;
default:
throw new ArgumentOutOfRangeException();
}
SetTriggerThreshold(_configuration.TriggerThreshold);
}
}
public GamepadStateSnapshot GetStateSnapshot()
{
return IGamepad.GetStateSnapshot(this);
}
public GamepadStateSnapshot GetMappedStateSnapshot()
{
GamepadStateSnapshot rawState = GetStateSnapshot();
GamepadStateSnapshot result = default;
lock (_userMappingLock)
{
if (_buttonsUserMapping.Count == 0)
return rawState;
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
{
if (!entry.IsValid)
continue;
// Do not touch state of button already pressed
if (!result.IsPressed(entry.To))
{
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
}
}
(float leftStickX, float leftStickY) = rawState.GetStick(_stickUserMapping[(int)StickInputId.Left]);
(float rightStickX, float rightStickY) = rawState.GetStick(_stickUserMapping[(int)StickInputId.Right]);
result.SetStick(StickInputId.Left, leftStickX, leftStickY);
result.SetStick(StickInputId.Right, rightStickX, rightStickY);
}
return result;
}
private static float ConvertRawStickValue(short value)
{
const float ConvertRate = 1.0f / (short.MaxValue + 0.5f);
return value * ConvertRate;
}
private JoyconConfigControllerStick<GamepadInputId, Common.Configuration.Hid.Controller.StickInputId>
GetLogicalJoyStickConfig(StickInputId inputId)
{
switch (inputId)
{
case StickInputId.Left:
if (_configuration.RightJoyconStick.Joystick ==
Common.Configuration.Hid.Controller.StickInputId.Left)
return _configuration.RightJoyconStick;
else
return _configuration.LeftJoyconStick;
case StickInputId.Right:
if (_configuration.LeftJoyconStick.Joystick ==
Common.Configuration.Hid.Controller.StickInputId.Right)
return _configuration.LeftJoyconStick;
else
return _configuration.RightJoyconStick;
}
return null;
}
public (float, float) GetStick(StickInputId inputId)
{
if (inputId == StickInputId.Unbound)
return (0.0f, 0.0f);
if (inputId == StickInputId.Left && _joyConType == JoyConType.Right || inputId == StickInputId.Right && _joyConType == JoyConType.Left)
{
return (0.0f, 0.0f);
}
(short stickX, short stickY) = GetStickXY();
float resultX = ConvertRawStickValue(stickX);
float resultY = -ConvertRawStickValue(stickY);
if (HasConfiguration)
{
var joyconStickConfig = GetLogicalJoyStickConfig(inputId);
if (joyconStickConfig != null)
{
if (joyconStickConfig.InvertStickX)
resultX = -resultX;
if (joyconStickConfig.InvertStickY)
resultY = -resultY;
if (joyconStickConfig.Rotate90CW)
{
float temp = resultX;
resultX = resultY;
resultY = -temp;
}
}
}
return inputId switch
{
StickInputId.Left when _joyConType == JoyConType.Left => (resultY, -resultX),
StickInputId.Right when _joyConType == JoyConType.Right => (-resultY, resultX),
_ => (0.0f, 0.0f)
};
}
private (short, short) GetStickXY()
{
return (
SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTX),
SDL_GameControllerGetAxis(_gamepadHandle, SDL_GameControllerAxis.SDL_CONTROLLER_AXIS_LEFTY));
}
public bool IsPressed(GamepadButtonInputId inputId)
{
if (!_buttonsDriverMapping.TryGetValue(inputId, out var button))
{
return false;
}
return SDL_GameControllerGetButton(_gamepadHandle, button) == 1;
}
}
}

View File

@@ -0,0 +1,142 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using static SDL2.SDL;
namespace Ryujinx.Input.SDL2
{
internal class SDL2JoyConPair(IGamepad left, IGamepad right) : IGamepad
{
private StandardControllerInputConfig _configuration;
private readonly StickInputId[] _stickUserMapping =
[
StickInputId.Unbound,
StickInputId.Left,
StickInputId.Right
];
public GamepadFeaturesFlag Features => (left?.Features ?? GamepadFeaturesFlag.None) |
(right?.Features ?? GamepadFeaturesFlag.None);
public const string Id = "JoyConPair";
string IGamepad.Id => Id;
public string Name => "* Nintendo Switch Joy-Con (L/R)";
public bool IsConnected => left is { IsConnected: true } && right is { IsConnected: true };
public void Dispose()
{
left?.Dispose();
right?.Dispose();
}
public GamepadStateSnapshot GetMappedStateSnapshot()
{
return GetStateSnapshot();
}
public Vector3 GetMotionData(MotionInputId inputId)
{
return inputId switch
{
MotionInputId.Accelerometer or
MotionInputId.Gyroscope => left.GetMotionData(inputId),
MotionInputId.SecondAccelerometer => right.GetMotionData(MotionInputId.Accelerometer),
MotionInputId.SecondGyroscope => right.GetMotionData(MotionInputId.Gyroscope),
_ => Vector3.Zero
};
}
public GamepadStateSnapshot GetStateSnapshot()
{
return IGamepad.GetStateSnapshot(this);
}
public (float, float) GetStick(StickInputId inputId)
{
return inputId switch
{
StickInputId.Left => left.GetStick(StickInputId.Left),
StickInputId.Right => right.GetStick(StickInputId.Right),
_ => (0, 0)
};
}
public bool IsPressed(GamepadButtonInputId inputId)
{
return left.IsPressed(inputId) || right.IsPressed(inputId);
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
if (lowFrequency != 0)
{
right.Rumble(lowFrequency, lowFrequency, durationMs);
}
if (highFrequency != 0)
{
left.Rumble(highFrequency, highFrequency, durationMs);
}
if (lowFrequency == 0 && highFrequency == 0)
{
left.Rumble(0, 0, durationMs);
right.Rumble(0, 0, durationMs);
}
}
public void SetConfiguration(InputConfig configuration)
{
left.SetConfiguration(configuration);
right.SetConfiguration(configuration);
}
public void SetTriggerThreshold(float triggerThreshold)
{
left.SetTriggerThreshold(triggerThreshold);
right.SetTriggerThreshold(triggerThreshold);
}
public static bool IsCombinable(List<string> gamepadsIds)
{
(int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds);
return leftIndex >= 0 && rightIndex >= 0;
}
private static (int leftIndex, int rightIndex) DetectJoyConPair(List<string> gamepadsIds)
{
var gamepadNames = gamepadsIds.Where(gamepadId => gamepadId != Id)
.Select((_, index) => SDL_GameControllerNameForIndex(index)).ToList();
int leftIndex = gamepadNames.IndexOf(SDL2JoyCon.LeftName);
int rightIndex = gamepadNames.IndexOf(SDL2JoyCon.RightName);
return (leftIndex, rightIndex);
}
public static IGamepad GetGamepad(List<string> gamepadsIds)
{
(int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds);
if (leftIndex == -1 || rightIndex == -1)
{
return null;
}
nint leftGamepadHandle = SDL_GameControllerOpen(leftIndex);
nint rightGamepadHandle = SDL_GameControllerOpen(rightIndex);
if (leftGamepadHandle == nint.Zero || rightGamepadHandle == nint.Zero)
{
return null;
}
return new SDL2JoyConPair(new SDL2JoyCon(leftGamepadHandle, gamepadsIds[leftIndex]),
new SDL2JoyCon(rightGamepadHandle, gamepadsIds[rightIndex]));
}
}
}

View File

@@ -266,6 +266,7 @@ namespace Ryujinx.Input.HLE
if (motionConfig.MotionBackend != MotionInputBackendType.CemuHook)
{
_leftMotionInput = new MotionInput();
_rightMotionInput = new MotionInput();
}
else
{
@@ -298,7 +299,20 @@ namespace Ryujinx.Input.HLE
if (controllerConfig.ControllerType == ConfigControllerType.JoyconPair)
{
_rightMotionInput = _leftMotionInput;
if (gamepad.Id== "JoyConPair")
{
Vector3 rightAccelerometer = gamepad.GetMotionData(MotionInputId.SecondAccelerometer);
Vector3 rightGyroscope = gamepad.GetMotionData(MotionInputId.SecondGyroscope);
rightAccelerometer = new Vector3(rightAccelerometer.X, -rightAccelerometer.Z, rightAccelerometer.Y);
rightGyroscope = new Vector3(rightGyroscope.X, -rightGyroscope.Z, rightGyroscope.Y);
_rightMotionInput.Update(rightAccelerometer, rightGyroscope, (ulong)PerformanceCounter.ElapsedNanoseconds / 1000, controllerConfig.Motion.Sensitivity, (float)controllerConfig.Motion.GyroDeadzone);
}
else
{
_rightMotionInput = _leftMotionInput;
}
}
}
}
@@ -333,6 +347,7 @@ namespace Ryujinx.Input.HLE
// Reset states
State = default;
_leftMotionInput = null;
_rightMotionInput = null;
}
}

View File

@@ -21,5 +21,17 @@ namespace Ryujinx.Input
/// </summary>
/// <remarks>Values are in degrees</remarks>
Gyroscope,
/// <summary>
/// Second accelerometer.
/// </summary>
/// <remarks>Values are in m/s^2</remarks>
SecondAccelerometer,
/// <summary>
/// Second gyroscope.
/// </summary>
/// <remarks>Values are in degrees</remarks>
SecondGyroscope
}
}

View File

@@ -25,14 +25,17 @@ namespace Ryujinx.SDL2.Common
public static Action<Action> MainThreadDispatcher { get; set; }
private const uint SdlInitFlags = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_AUDIO | SDL_INIT_VIDEO;
private const uint SdlInitFlags = SDL_INIT_EVENTS | SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK |
SDL_INIT_AUDIO | SDL_INIT_VIDEO;
private bool _isRunning;
private uint _refereceCount;
private Thread _worker;
private const uint SDL_JOYBATTERYUPDATED = 1543;
public event Action<int, int> OnJoyStickConnected;
public event Action<int> OnJoystickDisconnected;
public event Action<int, SDL_JoystickPowerLevel> OnJoyBatteryUpdated;
private ConcurrentDictionary<uint, Action<SDL_Event>> _registeredWindowHandlers;
@@ -78,12 +81,14 @@ namespace Ryujinx.SDL2.Common
// First ensure that we only enable joystick events (for connected/disconnected).
if (SDL_GameControllerEventState(SDL_IGNORE) != SDL_IGNORE)
{
Logger.Error?.PrintMsg(LogClass.Application, "Couldn't change the state of game controller events.");
Logger.Error?.PrintMsg(LogClass.Application,
"Couldn't change the state of game controller events.");
}
if (SDL_JoystickEventState(SDL_ENABLE) < 0)
{
Logger.Error?.PrintMsg(LogClass.Application, $"Failed to enable joystick event polling: {SDL_GetError()}");
Logger.Error?.PrintMsg(LogClass.Application,
$"Failed to enable joystick event polling: {SDL_GetError()}");
}
// Disable all joysticks information, we don't need them no need to flood the event queue for that.
@@ -143,7 +148,12 @@ namespace Ryujinx.SDL2.Common
OnJoystickDisconnected?.Invoke(evnt.cbutton.which);
}
else if (evnt.type is SDL_EventType.SDL_WINDOWEVENT or SDL_EventType.SDL_MOUSEBUTTONDOWN or SDL_EventType.SDL_MOUSEBUTTONUP)
else if ((uint)evnt.type == SDL_JOYBATTERYUPDATED)
{
OnJoyBatteryUpdated?.Invoke(evnt.cbutton.which, (SDL_JoystickPowerLevel)evnt.user.code);
}
else if (evnt.type is SDL_EventType.SDL_WINDOWEVENT or SDL_EventType.SDL_MOUSEBUTTONDOWN
or SDL_EventType.SDL_MOUSEBUTTONUP)
{
if (_registeredWindowHandlers.TryGetValue(evnt.window.windowID, out Action<SDL_Event> handler))
{

View File

@@ -1,36 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>
<None Remove="Resources\Controller_JoyConLeft.svg" />
<None Remove="Resources\Controller_JoyConPair.svg" />
<None Remove="Resources\Controller_JoyConRight.svg" />
<None Remove="Resources\Controller_ProCon.svg" />
<None Remove="Resources\Icon_NCA.png" />
<None Remove="Resources\Icon_NRO.png" />
<None Remove="Resources\Icon_NSO.png" />
<None Remove="Resources\Icon_NSP.png" />
<None Remove="Resources\Icon_XCI.png" />
<None Remove="Resources\Logo_Amiibo.png" />
<None Remove="Resources\Logo_Discord.png" />
<None Remove="Resources\Logo_GitHub.png" />
<None Remove="Resources\Logo_Ryujinx.png" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DiscordRichPresence" />
<PackageReference Include="DynamicData" />
<PackageReference Include="securifybv.ShellLink" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
<ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
</ItemGroup>
</Project>

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
using DiscordRPC;
using Gommon;
using Humanizer;
using Humanizer.Localisation;
using Ryujinx.Ava.Utilities.AppLibrary;
@@ -45,16 +46,7 @@ namespace Ryujinx.Ava
};
ConfigurationState.Instance.EnableDiscordIntegration.Event += Update;
TitleIDs.CurrentApplication.Event += (_, e) =>
{
if (e.NewValue)
SwitchToPlayingState(
ApplicationLibrary.LoadAndSaveMetaData(e.NewValue),
Switch.Shared.Processes.ActiveApplication
);
else
SwitchToMainState();
};
TitleIDs.CurrentApplication.Event += (_, e) => Use(e.NewValue);
}
private static void Update(object sender, ReactiveEventArgs<bool> evnt)
@@ -75,11 +67,23 @@ namespace Ryujinx.Ava
_discordClient = new DiscordRpcClient(ApplicationId);
_discordClient.Initialize();
_discordClient.SetPresence(_discordPresenceMain);
Use(TitleIDs.CurrentApplication);
}
}
}
public static void Use(Optional<string> titleId)
{
if (titleId.TryGet(out string tid))
SwitchToPlayingState(
ApplicationLibrary.LoadAndSaveMetaData(tid),
Switch.Shared.Processes.ActiveApplication
);
else
SwitchToMainState();
}
private static void SwitchToPlayingState(ApplicationMetadata appMeta, ProcessResult procRes)
{
_discordClient?.SetPresence(new RichPresence

View File

@@ -228,8 +228,6 @@ namespace Ryujinx.Headless
_inputConfiguration ??= [];
_enableKeyboard = option.EnableKeyboard;
_enableMouse = option.EnableMouse;
LoadPlayerConfiguration(option.InputProfile1Name, option.InputId1, PlayerIndex.Player1);
LoadPlayerConfiguration(option.InputProfile2Name, option.InputId2, PlayerIndex.Player2);
@@ -301,7 +299,10 @@ namespace Ryujinx.Headless
_userChannelPersistence.ShouldRestart = false;
}
_inputManager.Dispose();
try
{
_inputManager.Dispose();
} catch {}
return;
@@ -338,12 +339,12 @@ namespace Ryujinx.Headless
{
string label = state switch
{
LoadState => $"PTC : {current}/{total}",
ShaderCacheState => $"Shaders : {current}/{total}",
_ => throw new ArgumentException($"Unknown Progress Handler type {typeof(T)}"),
LoadState => "PTC",
ShaderCacheState => "Shaders",
_ => throw new ArgumentException($"Unknown Progress Handler type {typeof(T)}")
};
Logger.Info?.Print(LogClass.Application, label);
Logger.Info?.Print(LogClass.Application, $"{label} : {current}/{total}");
}
private static WindowBase CreateWindow(Options options)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -1,21 +0,0 @@
using System;
namespace Ryujinx.Headless
{
class StatusUpdatedEventArgs(
string vSyncMode,
string dockedMode,
string aspectRatio,
string gameStatus,
string fifoStatus,
string gpuName)
: EventArgs
{
public string VSyncMode = vSyncMode;
public string DockedMode = dockedMode;
public string AspectRatio = aspectRatio;
public string GameStatus = gameStatus;
public string FifoStatus = fifoStatus;
public string GpuName = gpuName;
}
}

View File

@@ -26,7 +26,7 @@ namespace Ryujinx.Headless
bool ignoreControllerApplet)
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet) { }
public override SDL_WindowFlags GetWindowFlags() => SDL_WindowFlags.SDL_WINDOW_METAL;
public override SDL_WindowFlags WindowFlags => SDL_WindowFlags.SDL_WINDOW_METAL;
protected override void InitializeWindowRenderer()
{

View File

@@ -108,8 +108,7 @@ namespace Ryujinx.Headless
}
}
}
private readonly GraphicsDebugLevel _glLogLevel;
private SDL2OpenGLContext _openGLContext;
public OpenGLWindow(
@@ -121,15 +120,14 @@ namespace Ryujinx.Headless
bool ignoreControllerApplet)
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet)
{
_glLogLevel = glLogLevel;
}
public override SDL_WindowFlags GetWindowFlags() => SDL_WindowFlags.SDL_WINDOW_OPENGL;
public override SDL_WindowFlags WindowFlags => SDL_WindowFlags.SDL_WINDOW_OPENGL;
protected override void InitializeWindowRenderer()
{
// Ensure to not share this context with other contexts before this point.
SetupOpenGLAttributes(false, _glLogLevel);
SetupOpenGLAttributes(false, GlLogLevel);
nint context = SDL_GL_CreateContext(WindowHandle);
CheckResult(SDL_GL_SetSwapInterval(1));

View File

@@ -10,8 +10,6 @@ namespace Ryujinx.Headless
{
class VulkanWindow : WindowBase
{
private readonly GraphicsDebugLevel _glLogLevel;
public VulkanWindow(
InputManager inputManager,
GraphicsDebugLevel glLogLevel,
@@ -21,10 +19,9 @@ namespace Ryujinx.Headless
bool ignoreControllerApplet)
: base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet)
{
_glLogLevel = glLogLevel;
}
public override SDL_WindowFlags GetWindowFlags() => SDL_WindowFlags.SDL_WINDOW_VULKAN;
public override SDL_WindowFlags WindowFlags => SDL_WindowFlags.SDL_WINDOW_VULKAN;
protected override void InitializeWindowRenderer() { }

View File

@@ -1,12 +1,12 @@
using Humanizer;
using LibHac.Tools.Fs;
using Ryujinx.Ava;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging;
using Ryujinx.Graphics.GAL;
using Ryujinx.Graphics.GAL.Multithreading;
using Ryujinx.Graphics.Gpu;
using Ryujinx.Graphics.OpenGL;
using Ryujinx.HLE.HOS.Applets;
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
@@ -53,8 +53,6 @@ namespace Ryujinx.Headless
public Switch Device { get; private set; }
public IRenderer Renderer { get; private set; }
public event EventHandler<StatusUpdatedEventArgs> StatusUpdatedEvent;
protected nint WindowHandle { get; set; }
public IHostUITheme HostUITheme { get; }
@@ -72,7 +70,7 @@ namespace Ryujinx.Headless
protected SDL2MouseDriver MouseDriver;
private readonly InputManager _inputManager;
private readonly IKeyboard _keyboardInterface;
private readonly GraphicsDebugLevel _glLogLevel;
protected readonly GraphicsDebugLevel GlLogLevel;
private readonly Stopwatch _chrono;
private readonly long _ticksPerFrame;
private readonly CancellationTokenSource _gpuCancellationTokenSource;
@@ -104,7 +102,7 @@ namespace Ryujinx.Headless
NpadManager = _inputManager.CreateNpadManager();
TouchScreenManager = _inputManager.CreateTouchScreenManager();
_keyboardInterface = (IKeyboard)_inputManager.KeyboardDriver.GetGamepad("0");
_glLogLevel = glLogLevel;
GlLogLevel = glLogLevel;
_chrono = new Stopwatch();
_ticksPerFrame = Stopwatch.Frequency / TargetFps;
_gpuCancellationTokenSource = new CancellationTokenSource();
@@ -137,7 +135,7 @@ namespace Ryujinx.Headless
private void SetWindowIcon()
{
Stream iconStream = typeof(Program).Assembly.GetManifestResourceStream("HeadlessLogo");
Stream iconStream = EmbeddedResources.GetStream("Ryujinx/Assets/UIImages/Logo_Ryujinx.png");
byte[] iconBytes = new byte[iconStream!.Length];
if (iconStream.Read(iconBytes, 0, iconBytes.Length) != iconBytes.Length)
@@ -163,6 +161,8 @@ namespace Ryujinx.Headless
}
}
private StatusUpdatedEventArgs _lastStatus;
private void InitializeWindow()
{
var activeProcess = Device.Processes.ActiveApplication;
@@ -191,7 +191,7 @@ namespace Ryujinx.Headless
FullscreenFlag = SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP;
}
WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), Width, Height, DefaultFlags | FullscreenFlag | GetWindowFlags());
WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), SDL_WINDOWPOS_CENTERED_DISPLAY(DisplayId), Width, Height, DefaultFlags | FullscreenFlag | WindowFlags);
if (WindowHandle == nint.Zero)
{
@@ -246,7 +246,7 @@ namespace Ryujinx.Headless
protected abstract void SwapBuffers();
public abstract SDL_WindowFlags GetWindowFlags();
public abstract SDL_WindowFlags WindowFlags { get; }
private string GetGpuDriverName()
{
@@ -268,7 +268,7 @@ namespace Ryujinx.Headless
{
InitializeWindowRenderer();
Device.Gpu.Renderer.Initialize(_glLogLevel);
Device.Gpu.Renderer.Initialize(GlLogLevel);
InitializeRenderer();
@@ -308,21 +308,6 @@ namespace Ryujinx.Headless
if (_ticks >= _ticksPerFrame)
{
string dockedMode = Device.System.State.DockedMode ? "Docked" : "Handheld";
float scale = GraphicsConfig.ResScale;
if (scale != 1)
{
dockedMode += $" ({scale}x)";
}
StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs(
Device.VSyncMode.ToString(),
dockedMode,
Device.Configuration.AspectRatio.ToText(),
$"{Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
$"FIFO: {Device.Statistics.GetFifoPercent():0.00} %",
$"GPU: {_gpuDriverName}"));
_ticks = Math.Min(_ticks - _ticksPerFrame, _ticksPerFrame);
}
}

View File

@@ -21,6 +21,7 @@ using Ryujinx.Graphics.Vulkan.MoltenVK;
using Ryujinx.Headless;
using Ryujinx.SDL2.Common;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
@@ -243,16 +244,33 @@ namespace Ryujinx.Ava
: $"Launch Mode: {AppDataManager.Mode}");
}
internal static void ProcessUnhandledException(object sender, Exception ex, bool isTerminating)
internal static void ProcessUnhandledException(object sender, Exception initialException, bool isTerminating)
{
Logger.Log log = Logger.Error ?? Logger.Notice;
string message = $"Unhandled exception caught: {ex}";
// ReSharper disable once ConstantConditionalAccessQualifier
if (sender?.GetType()?.AsPrettyString() is { } senderName)
log.Print(LogClass.Application, message, senderName);
List<Exception> exceptions = [];
if (initialException is AggregateException ae)
{
exceptions.AddRange(ae.InnerExceptions);
}
else
log.PrintMsg(LogClass.Application, message);
{
exceptions.Add(initialException);
}
foreach (var e in exceptions)
{
string message = $"Unhandled exception caught: {e}";
// ReSharper disable once ConstantConditionalAccessQualifier
if (sender?.GetType()?.AsPrettyString() is { } senderName)
log.Print(LogClass.Application, message, senderName);
else
log.PrintMsg(LogClass.Application, message);
}
if (isTerminating)
Exit();

View File

@@ -166,7 +166,6 @@
<EmbeddedResource Include="Assets\UIImages\Logo_GitHub_Light.png" />
<EmbeddedResource Include="Assets\UIImages\Logo_Ryujinx.png" />
<EmbeddedResource Include="Assets\UIImages\Logo_Ryujinx_AntiAlias.png" />
<EmbeddedResource Include="Headless\Ryujinx.bmp" LogicalName="HeadlessLogo" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Assets\locales.json" />

View File

@@ -133,12 +133,13 @@
Spacing="5">
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding TimePlayedString}"
Text="{Binding LastPlayedString}"
TextAlignment="End"
TextWrapping="Wrap" />
<TextBlock
HorizontalAlignment="Stretch"
Text="{Binding LastPlayedString}"
Text="{Binding TimePlayedString}"
IsVisible="{Binding HasPlayedPreviously}"
TextAlignment="End"
TextWrapping="Wrap" />
<TextBlock

View File

@@ -7,6 +7,7 @@
Title="Ryujinx - Waiting"
SizeToContent="WidthAndHeight"
WindowStartupLocation="CenterOwner"
CanResize="False"
mc:Ignorable="d"
Focusable="True">
<Grid

View File

@@ -1,152 +1,53 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Configuration.Hid;
namespace Ryujinx.Ava.UI.Models.Input
{
public class HotkeyConfig : BaseModel
public partial class HotkeyConfig : BaseModel
{
private Key _toggleVSyncMode;
public Key ToggleVSyncMode
{
get => _toggleVSyncMode;
set
{
_toggleVSyncMode = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _toggleVSyncMode;
private Key _screenshot;
public Key Screenshot
{
get => _screenshot;
set
{
_screenshot = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _screenshot;
private Key _showUI;
public Key ShowUI
{
get => _showUI;
set
{
_showUI = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _showUI;
private Key _pause;
public Key Pause
{
get => _pause;
set
{
_pause = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _pause;
private Key _toggleMute;
public Key ToggleMute
{
get => _toggleMute;
set
{
_toggleMute = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _toggleMute;
private Key _resScaleUp;
public Key ResScaleUp
{
get => _resScaleUp;
set
{
_resScaleUp = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _resScaleUp;
private Key _resScaleDown;
public Key ResScaleDown
{
get => _resScaleDown;
set
{
_resScaleDown = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _resScaleDown;
private Key _volumeUp;
public Key VolumeUp
{
get => _volumeUp;
set
{
_volumeUp = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _volumeUp;
private Key _volumeDown;
public Key VolumeDown
{
get => _volumeDown;
set
{
_volumeDown = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _volumeDown;
private Key _customVSyncIntervalIncrement;
public Key CustomVSyncIntervalIncrement
{
get => _customVSyncIntervalIncrement;
set
{
_customVSyncIntervalIncrement = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _customVSyncIntervalIncrement;
private Key _customVSyncIntervalDecrement;
public Key CustomVSyncIntervalDecrement
{
get => _customVSyncIntervalDecrement;
set
{
_customVSyncIntervalDecrement = value;
OnPropertyChanged();
}
}
[ObservableProperty] private Key _customVSyncIntervalDecrement;
public HotkeyConfig(KeyboardHotkeys config)
{
if (config != null)
{
ToggleVSyncMode = config.ToggleVSyncMode;
Screenshot = config.Screenshot;
ShowUI = config.ShowUI;
Pause = config.Pause;
ToggleMute = config.ToggleMute;
ResScaleUp = config.ResScaleUp;
ResScaleDown = config.ResScaleDown;
VolumeUp = config.VolumeUp;
VolumeDown = config.VolumeDown;
CustomVSyncIntervalIncrement = config.CustomVSyncIntervalIncrement;
CustomVSyncIntervalDecrement = config.CustomVSyncIntervalDecrement;
}
if (config == null)
return;
ToggleVSyncMode = config.ToggleVSyncMode;
Screenshot = config.Screenshot;
ShowUI = config.ShowUI;
Pause = config.Pause;
ToggleMute = config.ToggleMute;
ResScaleUp = config.ResScaleUp;
ResScaleDown = config.ResScaleDown;
VolumeUp = config.VolumeUp;
VolumeDown = config.VolumeDown;
CustomVSyncIntervalIncrement = config.CustomVSyncIntervalIncrement;
CustomVSyncIntervalDecrement = config.CustomVSyncIntervalDecrement;
}
public KeyboardHotkeys GetConfig()
{
var config = new KeyboardHotkeys
public KeyboardHotkeys GetConfig() =>
new()
{
ToggleVSyncMode = ToggleVSyncMode,
Screenshot = Screenshot,
@@ -160,8 +61,5 @@ namespace Ryujinx.Ava.UI.Models.Input
CustomVSyncIntervalIncrement = CustomVSyncIntervalIncrement,
CustomVSyncIntervalDecrement = CustomVSyncIntervalDecrement,
};
return config;
}
}
}

View File

@@ -22,5 +22,22 @@ namespace Ryujinx.Ava.UI.Models
FifoStatus = fifoStatus;
ShaderCount = shaderCount;
}
public override bool Equals(object obj)
{
if (obj is not StatusUpdatedEventArgs suea) return false;
return
VSyncMode == suea.VSyncMode &&
VolumeStatus == suea.VolumeStatus &&
DockedMode == suea.DockedMode &&
AspectRatio == suea.AspectRatio &&
GameStatus == suea.GameStatus &&
FifoStatus == suea.FifoStatus &&
ShaderCount == suea.ShaderCount;
}
public override int GetHashCode()
=> HashCode.Combine(VSyncMode, VolumeStatus, AspectRatio, DockedMode, FifoStatus, GameStatus, ShaderCount);
}
}

View File

@@ -1,21 +1,13 @@
using Avalonia.Svg.Skia;
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.Ava.UI.Models.Input;
using Ryujinx.Ava.UI.Views.Input;
namespace Ryujinx.Ava.UI.ViewModels.Input
{
public class ControllerInputViewModel : BaseModel
public partial class ControllerInputViewModel : BaseModel
{
private GamepadInputConfig _config;
public GamepadInputConfig Config
{
get => _config;
set
{
_config = value;
OnPropertyChanged();
}
}
[ObservableProperty] private GamepadInputConfig _config;
private bool _isLeft;
public bool IsLeft
@@ -43,16 +35,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public bool HasSides => IsLeft ^ IsRight;
private SvgImage _image;
public SvgImage Image
{
get => _image;
set
{
_image = value;
OnPropertyChanged();
}
}
[ObservableProperty] private SvgImage _image;
public readonly InputViewModel ParentModel;

View File

@@ -1,9 +1,8 @@
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Svg.Skia;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Helpers;
@@ -32,7 +31,7 @@ using Key = Ryujinx.Common.Configuration.Hid.Key;
namespace Ryujinx.Ava.UI.ViewModels.Input
{
public class InputViewModel : BaseModel, IDisposable
public partial class InputViewModel : BaseModel, IDisposable
{
private const string Disabled = "disabled";
private const string ProControllerResource = "Ryujinx/Assets/Icons/Controller_ProCon.svg";
@@ -48,8 +47,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
private int _controller;
private string _controllerImage;
private int _device;
private object _configViewModel;
private string _profileName;
[ObservableProperty] private object _configViewModel;
[ObservableProperty] private string _profileName;
private bool _isLoaded;
private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
@@ -73,17 +72,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public bool IsModified { get; set; }
public event Action NotifyChangesEvent;
public object ConfigViewModel
{
get => _configViewModel;
set
{
_configViewModel = value;
OnPropertyChanged();
}
}
public PlayerIndex PlayerIdChoose
{
get => _playerIdChoose;
@@ -200,16 +188,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
}
}
public string ProfileName
{
get => _profileName; set
{
_profileName = value;
OnPropertyChanged();
}
}
public int Device
{
get => _device;

View File

@@ -1,20 +1,12 @@
using Avalonia.Svg.Skia;
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.Ava.UI.Models.Input;
namespace Ryujinx.Ava.UI.ViewModels.Input
{
public class KeyboardInputViewModel : BaseModel
public partial class KeyboardInputViewModel : BaseModel
{
private KeyboardInputConfig _config;
public KeyboardInputConfig Config
{
get => _config;
set
{
_config = value;
OnPropertyChanged();
}
}
[ObservableProperty] private KeyboardInputConfig _config;
private bool _isLeft;
public bool IsLeft
@@ -42,16 +34,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public bool HasSides => IsLeft ^ IsRight;
private SvgImage _image;
public SvgImage Image
{
get => _image;
set
{
_image = value;
OnPropertyChanged();
}
}
[ObservableProperty] private SvgImage _image;
public readonly InputViewModel ParentModel;

View File

@@ -1,93 +1,23 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Ryujinx.Ava.UI.ViewModels.Input
{
public class MotionInputViewModel : BaseModel
public partial class MotionInputViewModel : BaseModel
{
private int _slot;
public int Slot
{
get => _slot;
set
{
_slot = value;
OnPropertyChanged();
}
}
[ObservableProperty] private int _slot;
private int _altSlot;
public int AltSlot
{
get => _altSlot;
set
{
_altSlot = value;
OnPropertyChanged();
}
}
[ObservableProperty] private int _altSlot;
private string _dsuServerHost;
public string DsuServerHost
{
get => _dsuServerHost;
set
{
_dsuServerHost = value;
OnPropertyChanged();
}
}
[ObservableProperty] private string _dsuServerHost;
private int _dsuServerPort;
public int DsuServerPort
{
get => _dsuServerPort;
set
{
_dsuServerPort = value;
OnPropertyChanged();
}
}
[ObservableProperty] private int _dsuServerPort;
private bool _mirrorInput;
public bool MirrorInput
{
get => _mirrorInput;
set
{
_mirrorInput = value;
OnPropertyChanged();
}
}
[ObservableProperty] private bool _mirrorInput;
private int _sensitivity;
public int Sensitivity
{
get => _sensitivity;
set
{
_sensitivity = value;
OnPropertyChanged();
}
}
[ObservableProperty] private int _sensitivity;
private double _gryoDeadzone;
public double GyroDeadzone
{
get => _gryoDeadzone;
set
{
_gryoDeadzone = value;
OnPropertyChanged();
}
}
[ObservableProperty] private double _gyroDeadzone;
private bool _enableCemuHookMotion;
public bool EnableCemuHookMotion
{
get => _enableCemuHookMotion;
set
{
_enableCemuHookMotion = value;
OnPropertyChanged();
}
}
[ObservableProperty] private bool _enableCemuHookMotion;
}
}

View File

@@ -1,27 +1,11 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Ryujinx.Ava.UI.ViewModels.Input
{
public class RumbleInputViewModel : BaseModel
public partial class RumbleInputViewModel : BaseModel
{
private float _strongRumble;
public float StrongRumble
{
get => _strongRumble;
set
{
_strongRumble = value;
OnPropertyChanged();
}
}
[ObservableProperty] private float _strongRumble;
private float _weakRumble;
public float WeakRumble
{
get => _weakRumble;
set
{
_weakRumble = value;
OnPropertyChanged();
}
}
[ObservableProperty] private float _weakRumble;
}
}

View File

@@ -136,6 +136,12 @@
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageBrazilianPortuguese}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageSwedish}" />
</ComboBoxItem>
<ComboBoxItem>
<TextBlock Text="{ext:Locale SettingsTabSystemSystemLanguageNorwegian}" />
</ComboBoxItem>
</ComboBox>
</StackPanel>
<StackPanel

View File

@@ -32,6 +32,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
@@ -136,6 +137,8 @@ namespace Ryujinx.Ava.UI.Windows
base.OnApplyTemplate(e);
NotificationHelper.SetNotificationManager(this);
ShowIntelMacWarningAsync();
}
private void OnScalingChanged(object sender, EventArgs e)
@@ -164,7 +167,7 @@ namespace Ryujinx.Ava.UI.Windows
});
}
private void ApplicationLibrary_LdnGameDataReceived(object sender, LdnGameDataReceivedEventArgs e)
private void ApplicationLibrary_LdnGameDataReceived(LdnGameDataReceivedEventArgs e)
{
Dispatcher.UIThread.Post(() =>
{
@@ -408,13 +411,10 @@ namespace Ryujinx.Ava.UI.Windows
{
StatusBarView.VolumeStatus.Click += VolumeStatus_CheckedChanged;
ApplicationGrid.DataContext = ApplicationList.DataContext = ViewModel;
ApplicationGrid.ApplicationOpened += Application_Opened;
ApplicationGrid.DataContext = ViewModel;
ApplicationList.ApplicationOpened += Application_Opened;
ApplicationList.DataContext = ViewModel;
}
private void SetWindowSizePosition()
@@ -734,5 +734,22 @@ namespace Ryujinx.Ava.UI.Windows
(int)Symbol.Checkmark);
});
}
private static bool _intelMacWarningShown;
public static async Task ShowIntelMacWarningAsync()
{
if (!_intelMacWarningShown &&
(OperatingSystem.IsMacOS() &&
(RuntimeInformation.OSArchitecture == Architecture.X64 ||
RuntimeInformation.OSArchitecture == Architecture.X86)))
{
_intelMacWarningShown = true;
await Dispatcher.UIThread.InvokeAsync(async () => await ContentDialogHelper.CreateWarningDialog(
"Intel Mac Warning",
"Intel Macs are not supported and will not work properly.\nIf you continue, do not come to our Discord asking for support."));
}
}
}
}

View File

@@ -1,4 +1,6 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using FluentAvalonia.Core;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
@@ -23,6 +25,11 @@ namespace Ryujinx.Ava.UI.Windows
InitializeComponent();
Load();
#if DEBUG
this.AttachDevTools(new KeyGesture(Key.F12, KeyModifiers.Alt));
#endif
}
public SettingsWindow()

View File

@@ -37,6 +37,8 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed);
public bool HasPlayedPreviously => TimePlayedString != string.Empty;
public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed)?.Replace(" ", "\n");
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);

View File

@@ -45,7 +45,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
public const string DefaultLanPlayWebHost = "ryuldnweb.vudjun.com";
public Language DesiredLanguage { get; set; }
public event EventHandler<ApplicationCountUpdatedEventArgs> ApplicationCountUpdated;
public event EventHandler<LdnGameDataReceivedEventArgs> LdnGameDataReceived;
public event Action<LdnGameDataReceivedEventArgs> LdnGameDataReceived;
public readonly IObservableCache<ApplicationData, ulong> Applications;
public readonly IObservableCache<(TitleUpdateModel TitleUpdate, bool IsSelected), TitleUpdateModel> TitleUpdates;
@@ -779,7 +779,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
using HttpClient httpClient = new HttpClient();
string ldnGameDataArrayString = await httpClient.GetStringAsync($"https://{ldnWebHost}/api/public_games");
ldnGameDataArray = JsonHelper.Deserialize(ldnGameDataArrayString, _ldnDataSerializerContext.IEnumerableLdnGameData);
LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs
LdnGameDataReceived?.Invoke(new LdnGameDataReceivedEventArgs
{
LdnData = ldnGameDataArray
});
@@ -787,7 +787,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
catch (Exception ex)
{
Logger.Warning?.Print(LogClass.Application, $"Failed to fetch the public games JSON from the API. Player and game count in the game list will be unavailable.\n{ex.Message}");
LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs
LdnGameDataReceived?.Invoke(new LdnGameDataReceivedEventArgs
{
LdnData = Array.Empty<LdnGameData>()
});
@@ -795,7 +795,7 @@ namespace Ryujinx.Ava.Utilities.AppLibrary
}
else
{
LdnGameDataReceived?.Invoke(null, new LdnGameDataReceivedEventArgs
LdnGameDataReceived?.Invoke(new LdnGameDataReceivedEventArgs
{
LdnData = Array.Empty<LdnGameData>()
});

View File

@@ -1,4 +1,5 @@
using Gommon;
using Humanizer;
using nietras.SeparatedValues;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Common.Logging;
@@ -56,7 +57,7 @@ namespace Ryujinx.Ava.Utilities.Compat
? titleIdRow
: default(Optional<string>);
GameName = ColStr(row[indices.GameName]).Trim().Trim('"');
GameName = ColStr(row[indices.GameName]);
Labels = ColStr(row[indices.Labels]).Split(';');
Status = ColStr(row[indices.Status]).ToLower() switch
@@ -83,12 +84,14 @@ namespace Ryujinx.Ava.Utilities.Compat
public LocaleKeys? Status { get; }
public DateTime LastUpdated { get; }
public string LocalizedLastUpdated =>
LocaleManager.FormatDynamicValue(LocaleKeys.CompatibilityListLastUpdated, LastUpdated.Humanize());
public string LocalizedStatus => LocaleManager.Instance[Status!.Value];
public string FormattedTitleId => TitleId
.OrElse(new string(' ', 16));
public string FormattedIssueLabels => Labels
.Where(it => !it.StartsWithIgnoreCase("status"))
.Select(FormatLabelName)
.JoinToString(", ");
@@ -97,7 +100,9 @@ namespace Ryujinx.Ava.Utilities.Compat
StringBuilder sb = new("CompatibilityEntry: {");
sb.Append($"{nameof(GameName)}=\"{GameName}\", ");
sb.Append($"{nameof(TitleId)}={TitleId}, ");
sb.Append($"{nameof(Labels)}=\"{Labels}\", ");
sb.Append($"{nameof(Labels)}={
Labels.FormatCollection(it => $"\"{it}\"", separator: ", ", prefix: "[", suffix: "]")
}, ");
sb.Append($"{nameof(Status)}=\"{Status}\", ");
sb.Append($"{nameof(LastUpdated)}=\"{LastUpdated}\"");
sb.Append('}');

View File

@@ -44,8 +44,11 @@
ItemsSource="{Binding CurrentEntries}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:CompatibilityEntry}">
<Grid Width="750" ColumnDefinitions="Auto,Auto,Auto,*"
Margin="5">
<Grid Width="750"
Margin="5"
ColumnDefinitions="Auto,Auto,Auto,*"
Background="Transparent"
ToolTip.Tip="{Binding LocalizedLastUpdated}">
<TextBlock Grid.Column="0"
Text="{Binding GameName}"
Width="320"

View File

@@ -1,3 +1,5 @@
using Humanizer;
using Humanizer.Localisation;
using Ryujinx.Ava.Common.Locale;
using System;
using System.Globalization;
@@ -31,7 +33,7 @@ namespace Ryujinx.Ava.Utilities
Gigabytes = 9,
Terabytes = 10,
Petabytes = 11,
Exabytes = 12,
Exabytes = 12
}
private const double SizeBase10 = 1000;
@@ -48,22 +50,24 @@ namespace Ryujinx.Ava.Utilities
public static string FormatTimeSpan(TimeSpan? timeSpan)
{
if (!timeSpan.HasValue || timeSpan.Value.TotalSeconds < 1)
{
// Game was never played
return TimeSpan.Zero.ToString("c", CultureInfo.InvariantCulture);
}
return string.Empty;
if (timeSpan.Value.TotalSeconds < 60)
return timeSpan.Value.Humanize(1,
countEmptyUnits: false,
maxUnit: TimeUnit.Second,
minUnit: TimeUnit.Second);
if (timeSpan.Value.TotalDays < 1)
{
// Game was played for less than a day
return timeSpan.Value.ToString("c", CultureInfo.InvariantCulture);
}
// Game was played for more than a day
TimeSpan onlyTime = timeSpan.Value.Subtract(TimeSpan.FromDays(timeSpan.Value.Days));
string onlyTimeString = onlyTime.ToString("c", CultureInfo.InvariantCulture);
return $"{timeSpan.Value.Days}d, {onlyTimeString}";
if (timeSpan.Value.TotalMinutes < 60)
return timeSpan.Value.Humanize(1,
countEmptyUnits: false,
maxUnit: TimeUnit.Minute,
minUnit: TimeUnit.Minute);
return timeSpan.Value.Humanize(1,
countEmptyUnits: false,
maxUnit: TimeUnit.Hour,
minUnit: TimeUnit.Hour);
}
/// <summary>