Compare commits

..

65 Commits

Author SHA1 Message Date
IvonWei
9f25816eeb Merge 4f8bb06d8e into e676fd8b17 2025-01-19 21:49:50 +01:00
madwind
4f8bb06d8e merge 2025-01-19 11:10:03 +08:00
madwind
8fd97963e9 Merge remote-tracking branch 'origin/master2' into SDL3
# Conflicts:
#	src/Ryujinx/Headless/Windows/OpenGLWindow.cs
#	src/Ryujinx/Headless/Windows/WindowBase.cs
2025-01-19 11:01:09 +08:00
madwind
f7c70def34 SDL3-3.1.10 2025-01-17 21:26:40 +08:00
IvonWei
846f8940e5 Merge branch 'Ryubing:master' into SDL3 2025-01-17 21:12:15 +08:00
madwind
c05aa45f30 remove sdl2 text 2025-01-17 19:28:59 +08:00
madwind
8f09f4cec8 disable sdl2 build 2025-01-17 15:37:56 +08:00
madwind
55d7584d0f sdl3 2025-01-17 13:09:25 +08:00
madwind
0c25cc1a81 remove log 2025-01-16 21:51:05 +08:00
madwind
6e7c787cb4 update 2025-01-16 21:35:46 +08:00
madwind
cfbe22e9b8 update sdl3 2025-01-16 20:37:54 +08:00
madwind
868e5199c2 fix pro layout 2025-01-16 20:36:07 +08:00
IvonWei
48968f1195 Merge branch 'Ryubing:master' into SDL3 2025-01-16 17:36:09 +08:00
madwind
f56cd968e2 update 2025-01-16 17:35:35 +08:00
madwind
82664ef69b fix audiomix 2025-01-16 17:24:00 +08:00
madwind
e1cb957d7b remove unnecessary code 2025-01-15 23:26:56 +08:00
madwind
dfbcdfa83a sdl3 audio callback 2025-01-15 22:38:21 +08:00
madwind
37f4c1ea1a Merge branch 'master2' into SDL3
# Conflicts:
#	Directory.Packages.props
2025-01-15 18:47:41 +08:00
madwind
fbc5ccfa2c sdl3 audio 2025-01-15 17:23:03 +08:00
madwind
0bd62888a0 sdl3 audio 2025-01-15 12:08:08 +08:00
madwind
b01dcd1963 update 2025-01-15 00:27:57 +08:00
madwind
992fe0e64f JoyConPair setting 2025-01-15 00:27:13 +08:00
madwind
b8b3767613 show button 2025-01-14 18:36:01 +08:00
madwind
c4b9aedc0f update 2025-01-13 22:44:44 +08:00
madwind
106b37f91f Merge branch 'master' into SDL3
# Conflicts:
#	src/Ryujinx/UI/ViewModels/Input/ControllerInputViewModel.cs
2025-01-13 09:14:47 +08: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
madwind
f4e7482312 fix sdl3 motion 2025-01-11 23:10:08 +08:00
Evan Husted
4518666a04 Merge branch 'master' into master 2025-01-10 21:39:41 -06:00
madwind
241b0152ad update 2025-01-11 00:24:53 +08:00
madwind
91f70584ec update 2025-01-09 23:47:44 +08:00
madwind
2c26388dde SDL3 2025-01-09 21:36:39 +08:00
IvonWei
7df576b5d4 Merge branch 'Ryubing:master' into joycon 2025-01-09 18:47:07 +08:00
madwind
7532ba06ad Merge remote-tracking branch 'origin/joycon' into joycon 2025-01-08 10:14:53 +08:00
madwind
6d5dd49550 Merge branch 'DeadZone' into joycon
# Conflicts:
#	src/Ryujinx/Ryujinx.csproj
2025-01-08 10:13:47 +08:00
madwind
1e3718343c Merge branch 'DeadZone' into joycon
# Conflicts:
#	src/Ryujinx/Ryujinx.csproj
2025-01-08 09:50:57 +08:00
IvonWei
39e99ce271 Merge branch 'Ryubing:master' into joycon 2025-01-08 09:41:40 +08:00
madwind
270c5bd815 add public method: GetJoystickPosition 2025-01-03 17:03:59 +08:00
madwind
6945a05e45 restore single joy-con svg 2025-01-03 17:01:02 +08: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
madwind
de9e93606a Display position to help set the deadzone. 2024-12-31 11:08:32 +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
120 changed files with 12466 additions and 2091 deletions

4
.github/labeler.yml vendored
View File

@@ -18,10 +18,6 @@ gpu:
- changed-files:
- any-glob-to-any-file: ['src/Ryujinx.Graphics.Vulkan/**', 'src/Spv.Generator/**']
'graphics-backend:metal':
- changed-files:
- any-glob-to-any-file: ['src/Ryujinx.Graphics.Metal/**', 'src/Ryujinx.Graphics.Metal.SharpMetalExtensions/**']
gui:
- changed-files:
- any-glob-to-any-file: ['src/Ryujinx/**', 'src/Ryujinx.UI.Common/**', 'src/Ryujinx.UI.LocaleGenerator/**']

View File

@@ -202,7 +202,7 @@ jobs:
macos_release:
name: Release MacOS universal
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

View File

@@ -183,7 +183,7 @@ jobs:
macos_release:
name: Release MacOS universal
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

View File

@@ -14,10 +14,10 @@
<PackageVersion Include="Microsoft.Build.Utilities.Core" Version="17.12.6" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Projektanker.Icons.Avalonia" Version="9.4.0" />
<PackageVersion Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.4.0"/>
<PackageVersion Include="Projektanker.Icons.Avalonia.MaterialDesign" Version="9.4.0"/>
<PackageVersion Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.4.0" />
<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="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageVersion Include="Concentus" Version="2.2.2" />
<PackageVersion Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageVersion Include="DynamicData" Version="9.0.4" />
@@ -41,7 +41,6 @@
<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />
<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.2" />
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
<PackageVersion Include="Sep" Version="0.6.0" />
@@ -58,4 +57,4 @@
<PackageVersion Include="System.Management" Version="9.0.0" />
<PackageVersion Include="UnicornEngine.Unicorn" Version="2.0.2-rc1-fb78016" />
</ItemGroup>
</Project>
</Project>

View File

@@ -54,13 +54,12 @@ failing to meet this requirement may result in a poor gameplay experience or une
## Latest build
Stable builds are made every so often, based on the `master` branch, that then gets put into the releases you know and love.
Stable builds are made every so often onto a separate "release" branch that then gets put into the releases you know and love.
These stable builds exist so that the end user can get a more **enjoyable and stable experience**.
They are released every month or so, to ensure consistent updates, while not being an annoying amount of individual updates to download over the course of that month.
You can find the latest stable release [here](https://github.com/GreemDev/Ryujinx/releases/latest).
Canary builds are compiled automatically for each commit on the `master` branch.
Canary builds are compiled automatically for each commit on the master branch.
While we strive to ensure optimal stability and performance prior to pushing an update, these builds **may be unstable or completely broken**.
These canary builds are only recommended for experienced users.
@@ -110,7 +109,7 @@ If you are planning to contribute or just want to learn more about this project
- **Configuration**
The emulator has settings for enabling or disabling some logging, remapping controllers, and more.
You can configure all of them through the graphical interface or manually through the config file, `Config.json`, found in the Ryujinx data folder which can be accessed by clicking `Open Ryujinx Folder` under the File menu in the GUI.
You can configure all of them through the graphical interface or manually through the config file, `Config.json`, found in the user folder which can be accessed by clicking `Open Ryujinx Folder` under the File menu in the GUI.
## License

View File

@@ -95,6 +95,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
.github\workflows\release.yml = .github\workflows\release.yml
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.SDL3.Common", "src\Ryujinx.SDL3.Common\Ryujinx.SDL3.Common.csproj", "{7C70B441-F3D1-41FE-A648-19014BFB88D9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Input.SDL3", "src\Ryujinx.Input.SDL3\Ryujinx.Input.SDL3.csproj", "{7420A718-7E3C-42B5-82EA-74F6BEE0F1FB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.SDL3-CS", "src\Ryujinx.SDL3-CS\Ryujinx.SDL3-CS.csproj", "{ED2A7EA4-4098-47ED-BA87-EDB3537CFC10}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Audio.Backends.SDL3", "src\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj", "{027A38DC-774D-4CF7-A1C0-C510BFC4BD29}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -198,15 +206,12 @@ Global
{C16F112F-38C3-40BC-9F5F-4791112063D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C16F112F-38C3-40BC-9F5F-4791112063D6}.Release|Any CPU.Build.0 = Release|Any CPU
{DFAB6F2D-B9BF-4AFF-B22B-7684A328EBA3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DFAB6F2D-B9BF-4AFF-B22B-7684A328EBA3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DFAB6F2D-B9BF-4AFF-B22B-7684A328EBA3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DFAB6F2D-B9BF-4AFF-B22B-7684A328EBA3}.Release|Any CPU.Build.0 = Release|Any CPU
{2D5D3A1D-5730-4648-B0AB-06C53CB910C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2D5D3A1D-5730-4648-B0AB-06C53CB910C0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2D5D3A1D-5730-4648-B0AB-06C53CB910C0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2D5D3A1D-5730-4648-B0AB-06C53CB910C0}.Release|Any CPU.Build.0 = Release|Any CPU
{D99A395A-8569-4DB0-B336-900647890052}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D99A395A-8569-4DB0-B336-900647890052}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D99A395A-8569-4DB0-B336-900647890052}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D99A395A-8569-4DB0-B336-900647890052}.Release|Any CPU.Build.0 = Release|Any CPU
{BEE1C184-C9A4-410B-8DFC-FB74D5C93AEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
@@ -259,6 +264,22 @@ Global
{81EA598C-DBA1-40B0-8DA4-4796B78F2037}.Debug|Any CPU.Build.0 = Debug|Any CPU
{81EA598C-DBA1-40B0-8DA4-4796B78F2037}.Release|Any CPU.ActiveCfg = Release|Any CPU
{81EA598C-DBA1-40B0-8DA4-4796B78F2037}.Release|Any CPU.Build.0 = Release|Any CPU
{7C70B441-F3D1-41FE-A648-19014BFB88D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7C70B441-F3D1-41FE-A648-19014BFB88D9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7C70B441-F3D1-41FE-A648-19014BFB88D9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7C70B441-F3D1-41FE-A648-19014BFB88D9}.Release|Any CPU.Build.0 = Release|Any CPU
{7420A718-7E3C-42B5-82EA-74F6BEE0F1FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7420A718-7E3C-42B5-82EA-74F6BEE0F1FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7420A718-7E3C-42B5-82EA-74F6BEE0F1FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7420A718-7E3C-42B5-82EA-74F6BEE0F1FB}.Release|Any CPU.Build.0 = Release|Any CPU
{ED2A7EA4-4098-47ED-BA87-EDB3537CFC10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ED2A7EA4-4098-47ED-BA87-EDB3537CFC10}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ED2A7EA4-4098-47ED-BA87-EDB3537CFC10}.Release|Any CPU.Build.0 = Release|Any CPU
{ED2A7EA4-4098-47ED-BA87-EDB3537CFC10}.Debug|Any CPU.Build.0 = Debug|Any CPU
{027A38DC-774D-4CF7-A1C0-C510BFC4BD29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{027A38DC-774D-4CF7-A1C0-C510BFC4BD29}.Debug|Any CPU.Build.0 = Debug|Any CPU
{027A38DC-774D-4CF7-A1C0-C510BFC4BD29}.Release|Any CPU.ActiveCfg = Release|Any CPU
{027A38DC-774D-4CF7-A1C0-C510BFC4BD29}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -19,7 +19,7 @@
<key>CSResourcesFileMapped</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2018 - 2025 Ryujinx Team and Contributors.</string>
<string>Copyright © 2018 - 2023 Ryujinx Team and Contributors.</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.games</string>
<key>LSMinimumSystemVersion</key>

View File

@@ -167,7 +167,7 @@
01006C40086EA000,"AeternoBlade",nvdec,playable,2020-12-14 20:06:48
0100B1C00949A000,"AeternoBlade Demo",nvdec,playable,2021-02-09 14:39:26
01009D100EA28000,"AeternoBlade II",online-broken;UE4;vulkan-backend-bug,playable,2022-09-12 21:11:18
0100B1C00949A000,"AeternoBlade II Demo Version",gpu;nvdec,ingame,2021-02-09 15:10:19
,"AeternoBlade II Demo Version",gpu;nvdec,ingame,2021-02-09 15:10:19
01001B400D334000,"AFL Evolution 2",slow;online-broken;UE4,playable,2022-12-07 12:45:56
0100DB100BBCE000,"Afterparty",,playable,2022-09-22 12:23:19
010087C011C4E000,"Agatha Christie - The ABC Murders",,playable,2020-10-27 17:08:23
@@ -477,7 +477,7 @@
010020700DE04000,"Bear With Me: The Lost Robots",nvdec,playable,2021-02-27 14:20:10
010024200E97E800,"Bear With Me: The Lost Robots Demo",nvdec,playable,2021-02-12 22:38:12
0100C0E014A4E000,"Bear's Restaurant",,playable,2024-08-11 21:26:59
010045F00BF64000,"BEAST Darling! ~Kemomimi Danshi to Himitsu no Ryou~",crash,menus,2020-10-04 06:12:08
,"BEAST Darling! ~Kemomimi Danshi to Himitsu no Ryou~",crash,menus,2020-10-04 06:12:08
01009C300BB4C000,"Beat Cop",,playable,2021-01-06 19:26:48
01002D20129FC000,"Beat Me!",online-broken,playable,2022-10-16 21:59:26
01006B0014590000,"BEAUTIFUL DESOLATION",gpu;nvdec,ingame,2022-10-26 10:34:38
@@ -703,7 +703,7 @@
01006A30124CA000,"Chocobo GP",gpu;crash,ingame,2022-06-04 14:52:18
0100BF600BF26000,"Chocobo's Mystery Dungeon EVERY BUDDY!",slow,playable,2020-05-26 13:53:13
01000BA0132EA000,"Choices That Matter: And The Sun Went Out",,playable,2020-12-17 15:44:08
0100A1200CA3C000,"Chou no Doku Hana no Kusari: Taishou Irokoi Ibun",gpu;nvdec,ingame,2020-09-28 17:58:04
,"Chou no Doku Hana no Kusari: Taishou Irokoi Ibun",gpu;nvdec,ingame,2020-09-28 17:58:04
010039A008E76000,"ChromaGun",,playable,2020-05-26 12:56:42
010006800E13A000,"Chronos: Before the Ashes",UE4;gpu;nvdec,ingame,2020-12-11 22:16:35
010039700BA7E000,"Circle of Sumo",,playable,2020-05-22 12:45:21
@@ -769,7 +769,7 @@
0100CCB01B1A0000,"COSMIC FANTASY COLLECTION",,ingame,2024-05-21 17:56:37
010067C00A776000,"Cosmic Star Heroine",,playable,2021-02-20 14:30:47
01003DD00F94A000,"COTTOn Reboot! [ コットン リブート! ]",,playable,2022-05-24 16:29:24
010077001526E000,"Cotton/Guardian Saturn Tribute Games",gpu,boots,2022-11-27 21:00:51
,"Cotton/Guardian Saturn Tribute Games",gpu,boots,2022-11-27 21:00:51
01000E301107A000,"Couch Co-Op Bundle Vol. 2",nvdec,playable,2022-10-02 12:04:21
0100C1E012A42000,"Country Tales",,playable,2021-06-17 16:45:39
01003370136EA000,"Cozy Grove",gpu,ingame,2023-07-30 22:22:19
@@ -830,7 +830,7 @@
01003ED0099B0000,"Danger Mouse: The Danger Games",crash;online,boots,2022-07-22 15:49:45
0100EFA013E7C000,"Danger Scavenger",nvdec,playable,2021-04-17 15:53:04
0100417007F78000,"Danmaku Unlimited 3",,playable,2020-11-15 00:48:35
01000330105BE000,"Darius Cozmic Collection",,playable,2021-02-19 20:59:06
,"Darius Cozmic Collection",,playable,2021-02-19 20:59:06
010059C00BED4000,"Darius Cozmic Collection Special Edition",,playable,2022-07-22 16:26:50
010015800F93C000,"Dariusburst - Another Chronicle EX+",online,playable,2021-04-05 14:21:43
01003D301357A000,"Dark Arcana: The Carnival",gpu;slow,ingame,2022-02-19 08:52:28
@@ -859,7 +859,7 @@
010095A011A14000,"Deadly Days",,playable,2020-11-27 13:38:55
0100BAC011928000,"Deadly Premonition 2: A Blessing In Disguise",,playable,2021-06-15 14:12:36
0100EBE00F22E000,"Deadly Premonition Origins",32-bit;nvdec,playable,2024-03-25 12:47:46
010015600D814000,"Dear Magi - Mahou Shounen Gakka -",,playable,2020-11-22 16:45:16
,"Dear Magi - Mahou Shounen Gakka -",,playable,2020-11-22 16:45:16
01000D60126B6000,"Death and Taxes",,playable,2020-12-15 20:27:49
010012B011AB2000,"Death Come True",nvdec,playable,2021-06-10 22:30:49
0100F3B00CF32000,"Death Coming",crash,nothing,2022-02-06 07:43:03
@@ -902,13 +902,13 @@
010023600C704000,"Deponia",nvdec,playable,2021-01-26 17:17:19
0100ED700469A000,"Deru - The Art of Cooperation",,playable,2021-01-07 16:59:59
0100D4600D0E4000,"Descenders",gpu,ingame,2020-12-10 15:22:36
0100D870102BC000,"Desire remaster ver.",crash,boots,2021-01-17 02:34:37
,"Desire remaster ver.",crash,boots,2021-01-17 02:34:37
010069500DD86000,"Destiny Connect: Tick-Tock Travelers",UE4;gpu;nvdec,ingame,2020-12-16 12:20:36
01008BB011ED6000,"Destrobots",,playable,2021-03-06 14:37:05
01009E701356A000,"Destroy All Humans!",gpu;nvdec;UE4,ingame,2023-01-14 22:23:53
010030600E65A000,"Detective Dolittle",,playable,2021-03-02 14:03:59
01009C0009842000,"Detective Gallo",nvdec,playable,2022-07-24 11:51:04
01002D400B0F6000,"Detective Jinguji Saburo Prism of Eyes",,playable,2020-10-02 21:54:41
,"Detective Jinguji Saburo Prism of Eyes",,playable,2020-10-02 21:54:41
010007500F27C000,"Detective Pikachu™ Returns",,playable,2023-10-07 10:24:59
010031B00CF66000,"Devil Engine",,playable,2021-06-04 11:54:30
01002F000E8F2000,"Devil Kingdom",,playable,2023-01-31 08:58:44
@@ -1057,7 +1057,7 @@
01004F000B716000,"Edna & Harvey: The Breakout Anniversary Edition",crash;nvdec,ingame,2022-08-01 16:59:56
01002550129F0000,"Effie",,playable,2022-10-27 14:36:39
0100CC0010A46000,"Ego Protocol: Remastered",nvdec,playable,2020-12-16 20:16:35
01004CC00B352000,"Eiga Sumikko Gurashi Tobidasu Ehon to Himitsu no Ko Game de Asobo Ehon no Sekai",,playable,2020-11-12 00:11:50
,"Eiga Sumikko Gurashi Tobidasu Ehon to Himitsu no Ko Game de Asobo Ehon no Sekai",,playable,2020-11-12 00:11:50
01003AD013BD2000,"Eight Dragons",nvdec,playable,2022-10-27 14:47:28
010020A01209C000,"El Hijo - A Wild West Tale",nvdec,playable,2021-04-19 17:44:08
0100B5B00EF38000,"Elden: Path of the Forgotten",,playable,2020-12-15 00:33:19
@@ -1070,7 +1070,6 @@
010017B0102A8000,"Emma: Lost in Memories",nvdec,playable,2021-01-28 16:19:10
010068300E08E000,"Enchanted in the Moonlight - Kiryu, Chikage & Yukinojo -",gpu;nvdec,ingame,2022-11-20 16:18:45
01007A4008486000,"Enchanting Mahjong Match",gpu,ingame,2020-04-17 22:01:31
0100EF901E552000,"ENDER MAGNOLIA: Bloom in the Mist",deadlock,boots,2025-01-22 17:59:00
01004F3011F92000,"Endless Fables: Dark Moor",gpu;nvdec,ingame,2021-03-07 15:31:03
010067B017588000,"Endless Ocean™ Luminous",services-horizon;crash,ingame,2024-05-30 02:05:57
0100B8700BD14000,"Energy Cycle Edge",services,ingame,2021-11-30 05:02:31
@@ -1124,7 +1123,7 @@
01005C10136CA000,"Fantasy Tavern Sextet -Vol.2 Adventurer's Days-",gpu;slow;crash,ingame,2021-11-06 02:57:29
010022700E7D6000,"FAR: Lone Sails",,playable,2022-09-06 16:33:05
0100C9E00FD62000,"Farabel",,playable,2020-08-03 17:47:28
0100ECD00C806000,"Farm Expert 2019 for Nintendo Switch",,playable,2020-07-09 21:42:57
,"Farm Expert 2019 for Nintendo Switch",,playable,2020-07-09 21:42:57
01000E400ED98000,"Farm Mystery",nvdec,playable,2022-09-06 16:46:47
010086B00BB50000,"Farm Together",,playable,2021-01-19 20:01:19
0100EB600E914000,"Farming Simulator 20",nvdec,playable,2021-06-13 10:52:44
@@ -1247,12 +1246,12 @@
0100ECE00C0C4000,"Fury Unleashed",crash;services,ingame,2020-10-18 11:52:40
010070000ED9E000,"Fury Unleashed Demo",,playable,2020-10-08 20:09:21
0100E1F013674000,"FUSER™",nvdec;UE4,playable,2022-10-17 20:58:32
0100A7A015E4C000,"Fushigi no Gensokyo Lotus Labyrinth",Needs Update;audio;gpu;nvdec,ingame,2021-01-20 15:30:02
,"Fushigi no Gensokyo Lotus Labyrinth",Needs Update;audio;gpu;nvdec,ingame,2021-01-20 15:30:02
01003C300B274000,"Futari de! Nyanko Daisensou",,playable,2024-01-05 22:26:52
010055801134E000,"FUZE Player",online-broken;vulkan-backend-bug,ingame,2022-10-18 12:23:53
0100EAD007E98000,"FUZE4 Nintendo Switch",vulkan-backend-bug,playable,2022-09-06 19:25:01
010067600F1A0000,"FuzzBall",crash,nothing,2021-03-29 20:13:21
0100275011e54000,"G-MODE Archives 06 The strongest ever Julia Miyamoto",,playable,2020-10-15 13:06:26
,"G-MODE Archives 06 The strongest ever Julia Miyamoto",,playable,2020-10-15 13:06:26
0100EB10108EA000,"G.I. Joe: Operation Blackout",UE4;crash,boots,2020-11-21 12:37:44
010048600B14E000,"Gal Metal",,playable,2022-07-27 20:57:48
010024700901A000,"Gal*Gun 2",nvdec;UE4,playable,2022-07-27 12:45:37
@@ -1371,7 +1370,7 @@
01006F80082E4000,"GUILTY GEAR XX ACCENT CORE PLUS R",nvdec,playable,2021-01-13 09:28:33
01003C6008940000,"GUNBIRD for Nintendo Switch",32-bit,playable,2021-06-04 19:16:01
0100BCB00AE98000,"GUNBIRD2 for Nintendo Switch",,playable,2020-10-10 14:41:16
01003FF010312000,"Gunka o haita neko",gpu;nvdec,ingame,2020-08-25 12:37:56
,"Gunka o haita neko",gpu;nvdec,ingame,2020-08-25 12:37:56
010061000D318000,"Gunman Clive HD Collection",,playable,2020-10-09 12:17:35
01006D4003BCE000,"Guns, Gore and Cannoli 2",online,playable,2021-01-06 18:43:59
01008C800E654000,"Gunvolt Chronicles Luminous Avenger iX - Retail Version",,playable,2020-06-16 22:47:07
@@ -1565,7 +1564,7 @@
0100BDC00A664000,"KAMEN RIDER CLIMAX SCRAMBLE",nvdec;ldn-untested,playable,2024-07-03 08:51:11
0100A9801180E000,"KAMEN RIDER memory of heroez / Premium Sound Edition",,playable,2022-12-06 03:14:26
010085300314E000,"KAMIKO",,playable,2020-05-13 12:48:57
010042C011736000,"Kangokuto Mary Skelter Finale",audio;crash,ingame,2021-01-09 22:39:28
,"Kangokuto Mary Skelter Finale",audio;crash,ingame,2021-01-09 22:39:28
01007FD00DB20000,"Katakoi Contrast - collection of branch -",nvdec,playable,2022-12-09 09:41:26
0100D7000C2C6000,"Katamari Damacy REROLL",,playable,2022-08-02 21:35:05
0100F9800EDFA000,"KATANA KAMI: A Way of the Samurai Story",slow,playable,2022-04-09 10:40:16
@@ -1582,7 +1581,7 @@
0100FB400D832000,"KILL la KILL -IF",,playable,2020-06-09 14:47:08
010011B00910C000,"Kill The Bad Guy",,playable,2020-05-12 22:16:10
0100F2900B3E2000,"Killer Queen Black",ldn-untested;online,playable,2021-04-08 12:46:18
010014A00C5E0000,"Kin'iro no Corda Octave",,playable,2020-09-22 13:23:12
,"Kin'iro no Corda Octave",,playable,2020-09-22 13:23:12
010089000F0E8000,"Kine",UE4,playable,2022-09-14 14:28:37
0100E6B00FFBA000,"King Lucas",,playable,2022-09-21 19:43:23
0100B1300783E000,"King Oddball",,playable,2020-05-13 13:47:57
@@ -1613,7 +1612,7 @@
01009EF00DDB4000,"Knockout City™",services;online-broken,boots,2022-12-09 09:48:58
0100C57019BA2000,"Koa and the Five Pirates of Mara",gpu,ingame,2024-07-11 16:14:44
01001E500401C000,"Koi DX",,playable,2020-05-11 21:37:51
010052300F612000,"Koi no Hanasaku Hyakkaen",32-bit;gpu;nvdec,ingame,2020-10-03 14:17:10
,"Koi no Hanasaku Hyakkaen",32-bit;gpu;nvdec,ingame,2020-10-03 14:17:10
01005D200C9AA000,"Koloro",,playable,2022-08-03 12:34:02
0100464009294000,"Kona",,playable,2022-08-03 12:48:19
010016C011AAA000,"Kono Subarashii Sekai ni Shukufuku o Kono Yokubo no Isho ni Choai o",,playable,2023-04-26 09:51:08
@@ -1780,8 +1779,8 @@
0100EC000CE24000,"Mech Rage",,playable,2020-11-18 12:30:16
0100C4F005EB4000,"Mecho Tales",,playable,2022-08-04 17:03:19
0100E4600D31A000,"Mechstermination Force",,playable,2024-07-04 05:39:15
01007580124C0000,"Medarot Classics Plus Kabuto Ver",,playable,2020-11-21 11:31:18
0100228012682000,"Medarot Classics Plus Kuwagata Ver",,playable,2020-11-21 11:30:40
,"Medarot Classics Plus Kabuto Ver",,playable,2020-11-21 11:31:18
,"Medarot Classics Plus Kuwagata Ver",,playable,2020-11-21 11:30:40
0100BBC00CB9A000,"Mega Mall Story",slow,playable,2022-08-04 17:10:58
0100B0C0086B0000,"Mega Man 11",,playable,2021-04-26 12:07:53
010038E016264000,"Mega Man Battle Network Legacy Collection Vol. 1",,playable,2023-04-25 03:55:57
@@ -1798,7 +1797,7 @@
0100B360068B2000,"Mekorama",gpu,boots,2021-06-17 16:37:21
01000FA010340000,"Melbits World",nvdec;online,menus,2021-11-26 13:51:22
0100F68019636000,"Melon Journey",,playable,2023-04-23 21:20:01
010079C012896000,"Memories Off -Innocent Fille- for Dearest",,playable,2020-08-04 07:31:22
,"Memories Off -Innocent Fille- for Dearest",,playable,2020-08-04 07:31:22
010062F011E7C000,"Memory Lane",UE4,playable,2022-10-05 14:31:03
0100EBE00D5B0000,"Meow Motors",UE4;gpu,ingame,2020-12-18 00:24:01
0100273008FBC000,"Mercenaries Saga Chronicles",,playable,2021-01-10 12:48:19
@@ -1874,7 +1873,7 @@
010093A01305C000,"Monster Hunter Rise Demo",online-broken;ldn-works;demo,playable,2022-10-18 23:04:17
0100E21011446000,"Monster Hunter Stories 2: Wings of Ruin",services,ingame,2022-07-10 19:27:30
010042501329E000,"MONSTER HUNTER STORIES 2: WINGS OF RUIN Trial Version",demo,playable,2022-11-13 22:20:26
0100C51003B46000,"Monster Hunter XX Demo",32-bit;cpu,nothing,2020-03-22 10:12:28
,"Monster Hunter XX Demo",32-bit;cpu,nothing,2020-03-22 10:12:28
0100C3800049C000,"Monster Hunter XX Nintendo Switch Ver ( Double Cross )",,playable,2024-07-21 14:08:09
010088400366E000,"Monster Jam Crush It!",UE4;nvdec;online,playable,2021-04-08 19:29:27
010095C00F354000,"Monster Jam Steel Titans",crash;nvdec;UE4,menus,2021-11-14 09:45:38
@@ -1918,7 +1917,7 @@
010035901046C000,"Mushroom Quest",,playable,2020-05-17 13:07:08
0100700006EF6000,"Mushroom Wars 2",nvdec,playable,2020-09-28 15:26:08
010046400F310000,"Music Racer",,playable,2020-08-10 08:51:23
0100153006300000,"Musou Orochi 2 Ultimate",crash;nvdec,boots,2021-04-09 19:39:16
,"Musou Orochi 2 Ultimate",crash;nvdec,boots,2021-04-09 19:39:16
0100F6000EAA8000,"Must Dash Amigos",,playable,2022-09-20 16:45:56
01007B6006092000,"MUSYNX",,playable,2020-05-08 14:24:43
0100C3E00ACAA000,"Mutant Football League: Dynasty Edition",online-broken,playable,2022-08-05 17:01:51
@@ -1949,7 +1948,7 @@
0100A6F00AC70000,"NAIRI: Tower of Shirin",nvdec,playable,2020-08-09 19:49:12
010002F001220000,"NAMCO MUSEUM",ldn-untested,playable,2024-08-13 07:52:21
0100DAA00AEE6000,"NAMCO MUSEUM™ ARCADE PAC™",,playable,2021-06-07 21:44:50
010039F010E14000,"NAMCOT COLLECTION",audio,playable,2020-06-25 13:35:22
,"NAMCOT COLLECTION",audio,playable,2020-06-25 13:35:22
010072B00BDDE000,"Narcos: Rise of the Cartels",UE4;crash;nvdec,boots,2021-03-22 13:18:47
01006BB00800A000,"NARUTO SHIPPUDEN: Ultimate Ninja STORM 3 Full Burst",nvdec,playable,2024-06-16 14:58:05
010084D00CF5E000,"NARUTO SHIPPUDEN™: Ultimate Ninja® STORM 4 ROAD TO BORUTO",,playable,2024-06-29 13:04:22
@@ -2090,11 +2089,11 @@
0100F9D00C186000,"Olympia Soiree",,playable,2022-12-04 21:07:12
0100A8B00E14A000,"Olympic Games Tokyo 2020 The Official Video Game™",ldn-untested;nvdec;online,playable,2021-01-06 01:20:24
01001D600E51A000,"Omega Labyrinth Life",,playable,2021-02-23 21:03:03
01005DE00CA34000,"Omega Vampire",nvdec,playable,2020-10-17 19:15:35
,"Omega Vampire",nvdec,playable,2020-10-17 19:15:35
0100CDC00C40A000,"Omensight: Definitive Edition",UE4;crash;nvdec,ingame,2020-07-26 01:45:14
01006DB00D970000,"OMG Zombies!",32-bit,playable,2021-04-12 18:04:45
010014E017B14000,"OMORI",,playable,2023-01-07 20:21:02
0100A5F011800000,"Once Upon A Coma",nvdec,playable,2020-08-01 12:09:39
,"Once Upon A Coma",nvdec,playable,2020-08-01 12:09:39
0100BD3006A02000,"One More Dungeon",,playable,2021-01-06 09:10:58
010076600FD64000,"One Person Story",,playable,2020-07-14 11:51:02
0100774009CF6000,"ONE PIECE Pirate Warriors 3 Deluxe Edition",nvdec,playable,2020-05-10 06:23:52
@@ -2185,7 +2184,7 @@
010062B01525C000,"Persona 4 Golden",,playable,2024-08-07 17:48:07
01005CA01580E000,"Persona 5 Royal",gpu,ingame,2024-08-17 21:45:15
010087701B092000,"Persona 5 Tactica",,playable,2024-04-01 22:21:03
0100E4F010D92000,"Persona 5: Scramble",deadlock,boots,2020-10-04 03:22:29
,"Persona 5: Scramble",deadlock,boots,2020-10-04 03:22:29
0100801011C3E000,"Persona® 5 Strikers",nvdec;mac-bug,playable,2023-09-26 09:36:01
010044400EEAE000,"Petoons Party",nvdec,playable,2021-03-02 21:07:58
010053401147C000,"PGA TOUR 2K21",deadlock;nvdec,ingame,2022-10-05 21:53:50
@@ -2274,7 +2273,7 @@
0100D1C01C194000,"Powerful Pro Baseball 2024-2025",gpu,ingame,2024-08-25 06:40:48
01008E100E416000,"PowerSlave Exhumed",gpu,ingame,2023-07-31 23:19:10
010054F01266C000,"Prehistoric Dude",gpu,ingame,2020-10-12 12:38:48
0100DB200D3E4000,"Pretty Princess Magical Coordinate",,playable,2020-10-15 11:43:41
,"Pretty Princess Magical Coordinate",,playable,2020-10-15 11:43:41
01007F00128CC000,"Pretty Princess Party",,playable,2022-10-19 17:23:58
010009300D278000,"Preventive Strike",nvdec,playable,2022-10-06 10:55:51
0100210019428000,"Prince of Persia The Lost Crown",crash,ingame,2024-06-08 21:31:58
@@ -2295,13 +2294,13 @@
0100ACE00DAB6000,"Project Nimbus: Complete Edition",nvdec;UE4;vulkan-backend-bug,playable,2022-08-10 17:35:43
01002980140F6000,"Project TRIANGLE STRATEGY™ Debut Demo",UE4;demo,playable,2022-10-24 21:40:27
0100BDB01150E000,"Project Warlock",,playable,2020-06-16 10:50:41
01009F100BC52000,"Psikyo Collection Vol 1",32-bit,playable,2020-10-11 13:18:47
,"Psikyo Collection Vol 1",32-bit,playable,2020-10-11 13:18:47
0100A2300DB78000,"Psikyo Collection Vol. 3",,ingame,2021-06-07 02:46:23
01009D400C4A8000,"Psikyo Collection Vol.2",32-bit,playable,2021-06-07 03:22:07
01007A200F2E2000,"Psikyo Shooting Stars Alpha",32-bit,playable,2021-04-13 12:03:43
0100D7400F2E4000,"Psikyo Shooting Stars Bravo",32-bit,playable,2021-06-14 12:09:07
0100EC100A790000,"PSYVARIAR DELTA",nvdec,playable,2021-01-20 13:01:46
0100AE700F184000,"Puchitto kurasutā",Need-Update;crash;services,menus,2020-07-04 16:44:28
,"Puchitto kurasutā",Need-Update;crash;services,menus,2020-07-04 16:44:28
0100D61010526000,"Pulstario",,playable,2022-10-06 11:02:01
01009AE00B788000,"Pumped BMX Pro",nvdec;online-broken,playable,2022-09-20 17:40:50
01006C10131F6000,"Pumpkin Jack",nvdec;UE4,playable,2022-10-13 12:52:32
@@ -2326,7 +2325,7 @@
0100DCF00F13A000,"Queen's Quest 4: Sacred Truce",nvdec,playable,2022-10-13 12:59:21
0100492012378000,"Quell",gpu,ingame,2021-06-11 15:59:53
01001DE005012000,"Quest of Dungeons",,playable,2021-06-07 10:29:22
010067D011E68000,"QuietMansion2",,playable,2020-09-03 14:59:35
,"QuietMansion2",,playable,2020-09-03 14:59:35
0100AF100EE76000,"Quiplash 2 InterLASHional: The Say Anything Party Game!",online-working,playable,2022-10-19 17:43:45
0100E5400BE64000,"R-Type Dimensions EX",,playable,2020-10-09 12:04:43
0100F930136B6000,"R-Type® Final 2",slow;nvdec;UE4,ingame,2022-10-30 21:46:29
@@ -2384,7 +2383,7 @@
01007A800D520000,"Refunct",UE4,playable,2020-12-15 22:46:21
0100FDF0083A6000,"Regalia: Of Men and Monarchs - Royal Edition",,playable,2022-08-11 12:24:01
01005FD00F15A000,"Regions of Ruin",,playable,2020-08-05 11:38:58
0100B5800C0E4000,"Reine des Fleurs",cpu;crash,boots,2020-09-27 18:50:39
,"Reine des Fleurs",cpu;crash,boots,2020-09-27 18:50:39
0100F1900B144000,"REKT! High Octane Stunts",online,playable,2020-09-28 12:33:56
01002AD013C52000,"Relicta",nvdec;UE4,playable,2022-10-31 12:48:33
010095900B436000,"RemiLore",,playable,2021-06-03 18:58:15
@@ -2496,7 +2495,7 @@
01002DF00F76C000,"SAMURAI SHODOWN",UE4;crash;nvdec,menus,2020-09-06 02:17:00
0100F6800F48E000,"SAMURAI SHODOWN NEOGEO COLLECTION",nvdec,playable,2021-06-14 17:12:56
0100B6501A360000,"Samurai Warrior",,playable,2023-02-27 18:42:38
01000EA00B23C000,"Sangoku Rensenki ~Otome no Heihou!~",gpu;nvdec,ingame,2020-10-17 19:13:14
,"Sangoku Rensenki ~Otome no Heihou!~",gpu;nvdec,ingame,2020-10-17 19:13:14
0100A4700BC98000,"Satsujin Tantei Jack the Ripper",,playable,2021-06-21 16:32:54
0100F0000869C000,"Saturday Morning RPG",nvdec,playable,2022-08-12 12:41:50
01006EE00380C000,"Sausage Sports Club",gpu,ingame,2021-01-10 05:37:17
@@ -2533,7 +2532,7 @@
010054400D2E6000,"SEGA AGES Virtua Racing",online-broken,playable,2023-01-29 17:08:39
01001E700AC60000,"SEGA AGES Wonder Boy: Monster Land",online,playable,2021-05-05 16:28:25
0100B3C014BDA000,"SEGA Genesis™ Nintendo Switch Online",crash;regression,nothing,2022-04-11 07:27:21
0100F7300B24E000,"SEGA Mega Drive Classics",online,playable,2021-01-05 11:08:00
,"SEGA Mega Drive Classics",online,playable,2021-01-05 11:08:00
01009840046BC000,"Semispheres",,playable,2021-01-06 23:08:31
0100D1800D902000,"SENRAN KAGURA Peach Ball",,playable,2021-06-03 15:12:10
0100E0C00ADAC000,"SENRAN KAGURA Reflexions",,playable,2020-03-23 19:15:23
@@ -2586,7 +2585,7 @@
0100B2E00F13E000,"Shipped",,playable,2020-11-21 14:22:32
01000E800FCB4000,"Ships",,playable,2021-06-11 16:14:37
01007430122D0000,"Shiren the Wanderer: The Tower of Fortune and the Dice of Fate",nvdec,playable,2022-10-20 11:44:36
010027300A660000,"Shiritsu Berubara Gakuen ~Versailles no Bara Re*imagination~",cpu;crash,boots,2020-09-27 19:01:25
,"Shiritsu Berubara Gakuen ~Versailles no Bara Re*imagination~",cpu;crash,boots,2020-09-27 19:01:25
01000244016BAE00,"Shiro0",gpu,ingame,2024-01-13 08:54:39
0100CCE00DDB6000,"Shoot 1UP DX",,playable,2020-12-13 12:32:47
01001180021FA000,"Shovel Knight: Specter of Torment",,playable,2020-05-30 08:34:17
@@ -2627,7 +2626,7 @@
0100C52011460000,"Sky: Children of the Light",cpu;online-broken,nothing,2023-02-23 10:57:10
010041C01014E000,"Skybolt Zack",,playable,2021-04-12 18:28:00
0100A0A00D1AA000,"SKYHILL",,playable,2021-03-05 15:19:11
0100CCC0002E6000,"Skylanders Imaginators",crash;services,boots,2020-05-30 18:49:18
,"Skylanders Imaginators",crash;services,boots,2020-05-30 18:49:18
010021A00ABEE000,"SKYPEACE",,playable,2020-05-29 14:14:30
0100EA400BF44000,"SkyScrappers",,playable,2020-05-28 22:11:25
0100F3C00C400000,"SkyTime",slow,ingame,2020-05-30 09:24:51
@@ -2682,7 +2681,7 @@
01005EA01C0FC000,"SONIC X SHADOW GENERATIONS",crash,ingame,2025-01-07 04:20:45
010064F00C212000,"Soul Axiom Rebooted",nvdec;slow,ingame,2020-09-04 12:41:01
0100F2100F0B2000,"Soul Searching",,playable,2020-07-09 18:39:07
01008F2005154000,"South Park™: The Fractured but Whole™ - Standard Edition",slow;online-broken;vulkan-backend-bug;gpu,ingame,2025-01-21 17:35:10
01008F2005154000,"South Park™: The Fractured but Whole™ - Standard Edition",slow;online-broken,playable,2024-07-08 17:47:28
0100B9F00C162000,"Space Blaze",,playable,2020-08-30 16:18:05
010005500E81E000,"Space Cows",UE4;crash,menus,2020-06-15 11:33:20
0100707011722000,"Space Elite Force",,playable,2020-11-27 15:21:05
@@ -2798,7 +2797,7 @@
0100681011B56000,"Struggling",,playable,2020-10-15 20:37:03
0100AF000B4AE000,"Stunt Kite Party",nvdec,playable,2021-01-25 17:16:56
0100C5500E7AE000,"STURMWIND EX",audio;32-bit,playable,2022-09-16 12:01:39
01001C1009892000,"Subarashiki Kono Sekai -Final Remix-",services;slow,ingame,2020-02-10 16:21:51
,"Subarashiki Kono Sekai -Final Remix-",services;slow,ingame,2020-02-10 16:21:51
010001400E474000,"Subdivision Infinity DX",UE4;crash,boots,2021-03-03 14:26:46
0100E6400BCE8000,"Sublevel Zero Redux",,playable,2022-09-16 12:30:03
0100EDA00D866000,"Submerged",nvdec;UE4;vulkan-backend-bug,playable,2022-08-16 15:17:01
@@ -2847,9 +2846,9 @@
0100284007D6C000,"Super One More Jump",,playable,2022-08-17 16:47:47
01001F90122B2000,"Super Punch Patrol",,playable,2024-07-12 19:49:02
0100331005E8E000,"Super Putty Squad",gpu;32-bit,ingame,2024-04-29 15:51:54
01006C900CC60000,"SUPER ROBOT WARS T",online,playable,2021-03-25 11:00:40
0100CA400E300000,"SUPER ROBOT WARS V",online,playable,2020-06-23 12:56:37
010026800E304000,"SUPER ROBOT WARS X",online,playable,2020-08-05 19:18:51
,"SUPER ROBOT WARS T",online,playable,2021-03-25 11:00:40
,"SUPER ROBOT WARS V",online,playable,2020-06-23 12:56:37
,"SUPER ROBOT WARS X",online,playable,2020-08-05 19:18:51
01004CF00A60E000,"Super Saurio Fly",nvdec,playable,2020-08-06 13:12:14
010039700D200000,"Super Skelemania",,playable,2020-06-07 22:59:50
01006A800016E000,"Super Smash Bros.™ Ultimate",gpu;crash;nvdec;ldn-works;intel-vendor-bug,ingame,2024-09-14 23:05:21
@@ -3015,7 +3014,7 @@
010085A00C5E8000,"The Lord of the Rings: Adventure Card Game - Definitive Edition",online-broken,menus,2022-09-16 15:19:32
01008A000A404000,"The Lost Child",nvdec,playable,2021-02-23 15:44:20
0100BAB00A116000,"The Low Road",,playable,2021-02-26 13:23:22
01005F3006AFE000,"The Mahjong",Needs Update;crash;services,nothing,2021-04-01 22:06:22
,"The Mahjong",Needs Update;crash;services,nothing,2021-04-01 22:06:22
0100DC300AC78000,"The Messenger",,playable,2020-03-22 13:51:37
0100DEC00B2BC000,"The Midnight Sanctuary",nvdec;UE4;vulkan-backend-bug,playable,2022-10-03 17:17:32
0100F1B00B456000,"The MISSING: J.J. Macfield and the Island of Memories",,playable,2022-08-22 19:36:18
@@ -3061,7 +3060,7 @@
010064E00ECBC000,"The Unicorn Princess",,playable,2022-09-16 16:20:56
0100BCF00E970000,"The Vanishing of Ethan Carter",UE4,playable,2021-06-09 17:14:47
0100D0500B0A6000,"The VideoKid",nvdec,playable,2021-01-06 09:28:24
01008CF00BA38000,"The Voice",services,menus,2020-07-28 20:48:49
,"The Voice",services,menus,2020-07-28 20:48:49
010056E00B4F4000,"The Walking Dead: A New Frontier",,playable,2022-09-21 13:40:48
010099100B6AC000,"The Walking Dead: Season Two",,playable,2020-08-09 12:57:06
010029200B6AA000,"The Walking Dead: The Complete First Season",,playable,2021-06-04 13:10:56
@@ -3377,7 +3376,7 @@
010022F00DA66000,"Yooka-Laylee and the Impossible Lair",,playable,2021-03-05 17:32:21
01006000040C2000,"Yoshis Crafted World™",gpu;audout,ingame,2021-08-30 13:25:51
0100AE800C9C6000,"Yoshis Crafted World™ Demo",gpu,boots,2020-12-16 14:57:40
0100BBA00B23E000,"Yoshiwara Higanbana Kuon no Chigiri",nvdec,playable,2020-10-17 19:14:46
,"Yoshiwara Higanbana Kuon no Chigiri",nvdec,playable,2020-10-17 19:14:46
01003A400C3DA800,"YouTube",,playable,2024-06-08 05:24:10
00100A7700CCAA40,"Youtubers Life00",nvdec,playable,2022-09-03 14:56:19
0100E390124D8000,"Ys IX: Monstrum Nox",,playable,2022-06-12 04:14:42
@@ -3387,7 +3386,7 @@
01002D60188DE000,"Yu-Gi-Oh! Rush Duel: Dawn of the Battle Royale!! Let's Go! Go Rush!!",crash,ingame,2023-03-17 01:54:01
010037D00DBDC000,"YU-NO: A girl who chants love at the bound of this world.",nvdec,playable,2021-01-26 17:03:52
0100B56011502000,"Yumeutsutsu Re:After",,playable,2022-11-20 16:09:06
0100DE200C0DA000,"Yunohana Spring! - Mellow Times -",audio;crash,menus,2020-09-27 19:27:40
,"Yunohana Spring! - Mellow Times -",audio;crash,menus,2020-09-27 19:27:40
0100307011C44000,"Yuppie Psycho: Executive Edition",crash,ingame,2020-12-11 10:37:06
0100FC900963E000,"Yuri",,playable,2021-06-11 13:08:50
010092400A678000,"Zaccaria Pinball",online-broken,playable,2022-09-03 15:44:28
@@ -3398,7 +3397,7 @@
0100AAC00E692000,"Zenith",,playable,2022-09-17 09:57:02
0100A6A00894C000,"ZERO GUNNER 2- for Nintendo Switch",,playable,2021-01-04 20:17:14
01004B001058C000,"Zero Strain",services;UE4,menus,2021-11-10 07:48:32
010021300F69E000,"Zettai kaikyu gakuen",gpu;nvdec,ingame,2020-08-25 15:15:54
,"Zettai kaikyu gakuen",gpu;nvdec,ingame,2020-08-25 15:15:54
0100D7B013DD0000,"Ziggy the Chaser",,playable,2021-02-04 20:34:27
010086700EF16000,"ZikSquare",gpu,ingame,2021-11-06 02:02:48
010069C0123D8000,"Zoids Wild Blast Unleashed",nvdec,playable,2022-10-15 11:26:59
@@ -3410,7 +3409,7 @@
0100CD300A1BA000,"Zombillie",,playable,2020-07-23 17:42:23
01001EE00A6B0000,"Zotrix: Solar Division",,playable,2021-06-07 20:34:05
0100B9B00C6A4000,"この世の果てで恋を唄う少女YU-NO",audio,ingame,2021-01-22 07:00:16
0100E8600C504000,"スーパーファミコン Nintendo Switch Online",slow,ingame,2020-03-14 05:48:38
,"スーパーファミコン Nintendo Switch Online",slow,ingame,2020-03-14 05:48:38
01000BB01CB8A000,"トラブル・マギア ~訳アリ少女は未来を勝ち取るために異国の魔法学校へ留学します~(Trouble Magia ~Wakeari Shoujo wa Mirai o Kachitoru Tame ni Ikoku no Mahou Gakkou e Ryuugaku Shimasu~)",,nothing,2024-09-28 07:03:14
010065500B218000,"メモリーズオフ - Innocent Fille",,playable,2022-12-02 17:36:48
010032400E700000,"二ノ国 白き聖灰の女王",services;32-bit,menus,2023-04-16 17:11:06
1 title_id game_name labels status last_updated
167 01006C40086EA000 AeternoBlade nvdec playable 2020-12-14 20:06:48
168 0100B1C00949A000 AeternoBlade Demo nvdec playable 2021-02-09 14:39:26
169 01009D100EA28000 AeternoBlade II online-broken;UE4;vulkan-backend-bug playable 2022-09-12 21:11:18
170 0100B1C00949A000 AeternoBlade II Demo Version gpu;nvdec ingame 2021-02-09 15:10:19
171 01001B400D334000 AFL Evolution 2 slow;online-broken;UE4 playable 2022-12-07 12:45:56
172 0100DB100BBCE000 Afterparty playable 2022-09-22 12:23:19
173 010087C011C4E000 Agatha Christie - The ABC Murders playable 2020-10-27 17:08:23
477 010020700DE04000 Bear With Me: The Lost Robots nvdec playable 2021-02-27 14:20:10
478 010024200E97E800 Bear With Me: The Lost Robots Demo nvdec playable 2021-02-12 22:38:12
479 0100C0E014A4E000 Bear's Restaurant playable 2024-08-11 21:26:59
480 010045F00BF64000 BEAST Darling! ~Kemomimi Danshi to Himitsu no Ryou~ crash menus 2020-10-04 06:12:08
481 01009C300BB4C000 Beat Cop playable 2021-01-06 19:26:48
482 01002D20129FC000 Beat Me! online-broken playable 2022-10-16 21:59:26
483 01006B0014590000 BEAUTIFUL DESOLATION gpu;nvdec ingame 2022-10-26 10:34:38
703 01006A30124CA000 Chocobo GP gpu;crash ingame 2022-06-04 14:52:18
704 0100BF600BF26000 Chocobo's Mystery Dungeon EVERY BUDDY! slow playable 2020-05-26 13:53:13
705 01000BA0132EA000 Choices That Matter: And The Sun Went Out playable 2020-12-17 15:44:08
706 0100A1200CA3C000 Chou no Doku Hana no Kusari: Taishou Irokoi Ibun gpu;nvdec ingame 2020-09-28 17:58:04
707 010039A008E76000 ChromaGun playable 2020-05-26 12:56:42
708 010006800E13A000 Chronos: Before the Ashes UE4;gpu;nvdec ingame 2020-12-11 22:16:35
709 010039700BA7E000 Circle of Sumo playable 2020-05-22 12:45:21
769 0100CCB01B1A0000 COSMIC FANTASY COLLECTION ingame 2024-05-21 17:56:37
770 010067C00A776000 Cosmic Star Heroine playable 2021-02-20 14:30:47
771 01003DD00F94A000 COTTOn Reboot! [ コットン リブート! ] playable 2022-05-24 16:29:24
772 010077001526E000 Cotton/Guardian Saturn Tribute Games gpu boots 2022-11-27 21:00:51
773 01000E301107A000 Couch Co-Op Bundle Vol. 2 nvdec playable 2022-10-02 12:04:21
774 0100C1E012A42000 Country Tales playable 2021-06-17 16:45:39
775 01003370136EA000 Cozy Grove gpu ingame 2023-07-30 22:22:19
830 01003ED0099B0000 Danger Mouse: The Danger Games crash;online boots 2022-07-22 15:49:45
831 0100EFA013E7C000 Danger Scavenger nvdec playable 2021-04-17 15:53:04
832 0100417007F78000 Danmaku Unlimited 3 playable 2020-11-15 00:48:35
833 01000330105BE000 Darius Cozmic Collection playable 2021-02-19 20:59:06
834 010059C00BED4000 Darius Cozmic Collection Special Edition playable 2022-07-22 16:26:50
835 010015800F93C000 Dariusburst - Another Chronicle EX+ online playable 2021-04-05 14:21:43
836 01003D301357A000 Dark Arcana: The Carnival gpu;slow ingame 2022-02-19 08:52:28
859 010095A011A14000 Deadly Days playable 2020-11-27 13:38:55
860 0100BAC011928000 Deadly Premonition 2: A Blessing In Disguise playable 2021-06-15 14:12:36
861 0100EBE00F22E000 Deadly Premonition Origins 32-bit;nvdec playable 2024-03-25 12:47:46
862 010015600D814000 Dear Magi - Mahou Shounen Gakka - playable 2020-11-22 16:45:16
863 01000D60126B6000 Death and Taxes playable 2020-12-15 20:27:49
864 010012B011AB2000 Death Come True nvdec playable 2021-06-10 22:30:49
865 0100F3B00CF32000 Death Coming crash nothing 2022-02-06 07:43:03
902 010023600C704000 Deponia nvdec playable 2021-01-26 17:17:19
903 0100ED700469A000 Deru - The Art of Cooperation playable 2021-01-07 16:59:59
904 0100D4600D0E4000 Descenders gpu ingame 2020-12-10 15:22:36
905 0100D870102BC000 Desire remaster ver. crash boots 2021-01-17 02:34:37
906 010069500DD86000 Destiny Connect: Tick-Tock Travelers UE4;gpu;nvdec ingame 2020-12-16 12:20:36
907 01008BB011ED6000 Destrobots playable 2021-03-06 14:37:05
908 01009E701356A000 Destroy All Humans! gpu;nvdec;UE4 ingame 2023-01-14 22:23:53
909 010030600E65A000 Detective Dolittle playable 2021-03-02 14:03:59
910 01009C0009842000 Detective Gallo nvdec playable 2022-07-24 11:51:04
911 01002D400B0F6000 Detective Jinguji Saburo Prism of Eyes playable 2020-10-02 21:54:41
912 010007500F27C000 Detective Pikachu™ Returns playable 2023-10-07 10:24:59
913 010031B00CF66000 Devil Engine playable 2021-06-04 11:54:30
914 01002F000E8F2000 Devil Kingdom playable 2023-01-31 08:58:44
1057 01004F000B716000 Edna & Harvey: The Breakout – Anniversary Edition crash;nvdec ingame 2022-08-01 16:59:56
1058 01002550129F0000 Effie playable 2022-10-27 14:36:39
1059 0100CC0010A46000 Ego Protocol: Remastered nvdec playable 2020-12-16 20:16:35
1060 01004CC00B352000 Eiga Sumikko Gurashi Tobidasu Ehon to Himitsu no Ko Game de Asobo Ehon no Sekai playable 2020-11-12 00:11:50
1061 01003AD013BD2000 Eight Dragons nvdec playable 2022-10-27 14:47:28
1062 010020A01209C000 El Hijo - A Wild West Tale nvdec playable 2021-04-19 17:44:08
1063 0100B5B00EF38000 Elden: Path of the Forgotten playable 2020-12-15 00:33:19
1070 010017B0102A8000 Emma: Lost in Memories nvdec playable 2021-01-28 16:19:10
1071 010068300E08E000 Enchanted in the Moonlight - Kiryu, Chikage & Yukinojo - gpu;nvdec ingame 2022-11-20 16:18:45
1072 01007A4008486000 Enchanting Mahjong Match gpu ingame 2020-04-17 22:01:31
0100EF901E552000 ENDER MAGNOLIA: Bloom in the Mist deadlock boots 2025-01-22 17:59:00
1073 01004F3011F92000 Endless Fables: Dark Moor gpu;nvdec ingame 2021-03-07 15:31:03
1074 010067B017588000 Endless Ocean™ Luminous services-horizon;crash ingame 2024-05-30 02:05:57
1075 0100B8700BD14000 Energy Cycle Edge services ingame 2021-11-30 05:02:31
1123 01005C10136CA000 Fantasy Tavern Sextet -Vol.2 Adventurer's Days- gpu;slow;crash ingame 2021-11-06 02:57:29
1124 010022700E7D6000 FAR: Lone Sails playable 2022-09-06 16:33:05
1125 0100C9E00FD62000 Farabel playable 2020-08-03 17:47:28
1126 0100ECD00C806000 Farm Expert 2019 for Nintendo Switch playable 2020-07-09 21:42:57
1127 01000E400ED98000 Farm Mystery nvdec playable 2022-09-06 16:46:47
1128 010086B00BB50000 Farm Together playable 2021-01-19 20:01:19
1129 0100EB600E914000 Farming Simulator 20 nvdec playable 2021-06-13 10:52:44
1246 0100ECE00C0C4000 Fury Unleashed crash;services ingame 2020-10-18 11:52:40
1247 010070000ED9E000 Fury Unleashed Demo playable 2020-10-08 20:09:21
1248 0100E1F013674000 FUSER™ nvdec;UE4 playable 2022-10-17 20:58:32
1249 0100A7A015E4C000 Fushigi no Gensokyo Lotus Labyrinth Needs Update;audio;gpu;nvdec ingame 2021-01-20 15:30:02
1250 01003C300B274000 Futari de! Nyanko Daisensou playable 2024-01-05 22:26:52
1251 010055801134E000 FUZE Player online-broken;vulkan-backend-bug ingame 2022-10-18 12:23:53
1252 0100EAD007E98000 FUZE4 Nintendo Switch vulkan-backend-bug playable 2022-09-06 19:25:01
1253 010067600F1A0000 FuzzBall crash nothing 2021-03-29 20:13:21
1254 0100275011e54000 G-MODE Archives 06 The strongest ever Julia Miyamoto playable 2020-10-15 13:06:26
1255 0100EB10108EA000 G.I. Joe: Operation Blackout UE4;crash boots 2020-11-21 12:37:44
1256 010048600B14E000 Gal Metal playable 2022-07-27 20:57:48
1257 010024700901A000 Gal*Gun 2 nvdec;UE4 playable 2022-07-27 12:45:37
1370 01006F80082E4000 GUILTY GEAR XX ACCENT CORE PLUS R nvdec playable 2021-01-13 09:28:33
1371 01003C6008940000 GUNBIRD for Nintendo Switch 32-bit playable 2021-06-04 19:16:01
1372 0100BCB00AE98000 GUNBIRD2 for Nintendo Switch playable 2020-10-10 14:41:16
1373 01003FF010312000 Gunka o haita neko gpu;nvdec ingame 2020-08-25 12:37:56
1374 010061000D318000 Gunman Clive HD Collection playable 2020-10-09 12:17:35
1375 01006D4003BCE000 Guns, Gore and Cannoli 2 online playable 2021-01-06 18:43:59
1376 01008C800E654000 Gunvolt Chronicles Luminous Avenger iX - Retail Version playable 2020-06-16 22:47:07
1564 0100BDC00A664000 KAMEN RIDER CLIMAX SCRAMBLE nvdec;ldn-untested playable 2024-07-03 08:51:11
1565 0100A9801180E000 KAMEN RIDER memory of heroez / Premium Sound Edition playable 2022-12-06 03:14:26
1566 010085300314E000 KAMIKO playable 2020-05-13 12:48:57
1567 010042C011736000 Kangokuto Mary Skelter Finale audio;crash ingame 2021-01-09 22:39:28
1568 01007FD00DB20000 Katakoi Contrast - collection of branch - nvdec playable 2022-12-09 09:41:26
1569 0100D7000C2C6000 Katamari Damacy REROLL playable 2022-08-02 21:35:05
1570 0100F9800EDFA000 KATANA KAMI: A Way of the Samurai Story slow playable 2022-04-09 10:40:16
1581 0100FB400D832000 KILL la KILL -IF playable 2020-06-09 14:47:08
1582 010011B00910C000 Kill The Bad Guy playable 2020-05-12 22:16:10
1583 0100F2900B3E2000 Killer Queen Black ldn-untested;online playable 2021-04-08 12:46:18
1584 010014A00C5E0000 Kin'iro no Corda Octave playable 2020-09-22 13:23:12
1585 010089000F0E8000 Kine UE4 playable 2022-09-14 14:28:37
1586 0100E6B00FFBA000 King Lucas playable 2022-09-21 19:43:23
1587 0100B1300783E000 King Oddball playable 2020-05-13 13:47:57
1612 01009EF00DDB4000 Knockout City™ services;online-broken boots 2022-12-09 09:48:58
1613 0100C57019BA2000 Koa and the Five Pirates of Mara gpu ingame 2024-07-11 16:14:44
1614 01001E500401C000 Koi DX playable 2020-05-11 21:37:51
1615 010052300F612000 Koi no Hanasaku Hyakkaen 32-bit;gpu;nvdec ingame 2020-10-03 14:17:10
1616 01005D200C9AA000 Koloro playable 2022-08-03 12:34:02
1617 0100464009294000 Kona playable 2022-08-03 12:48:19
1618 010016C011AAA000 Kono Subarashii Sekai ni Shukufuku o Kono Yokubo no Isho ni Choai o playable 2023-04-26 09:51:08
1779 0100EC000CE24000 Mech Rage playable 2020-11-18 12:30:16
1780 0100C4F005EB4000 Mecho Tales playable 2022-08-04 17:03:19
1781 0100E4600D31A000 Mechstermination Force playable 2024-07-04 05:39:15
1782 01007580124C0000 Medarot Classics Plus Kabuto Ver playable 2020-11-21 11:31:18
1783 0100228012682000 Medarot Classics Plus Kuwagata Ver playable 2020-11-21 11:30:40
1784 0100BBC00CB9A000 Mega Mall Story slow playable 2022-08-04 17:10:58
1785 0100B0C0086B0000 Mega Man 11 playable 2021-04-26 12:07:53
1786 010038E016264000 Mega Man Battle Network Legacy Collection Vol. 1 playable 2023-04-25 03:55:57
1797 0100B360068B2000 Mekorama gpu boots 2021-06-17 16:37:21
1798 01000FA010340000 Melbits World nvdec;online menus 2021-11-26 13:51:22
1799 0100F68019636000 Melon Journey playable 2023-04-23 21:20:01
1800 010079C012896000 Memories Off -Innocent Fille- for Dearest playable 2020-08-04 07:31:22
1801 010062F011E7C000 Memory Lane UE4 playable 2022-10-05 14:31:03
1802 0100EBE00D5B0000 Meow Motors UE4;gpu ingame 2020-12-18 00:24:01
1803 0100273008FBC000 Mercenaries Saga Chronicles playable 2021-01-10 12:48:19
1873 010093A01305C000 Monster Hunter Rise Demo online-broken;ldn-works;demo playable 2022-10-18 23:04:17
1874 0100E21011446000 Monster Hunter Stories 2: Wings of Ruin services ingame 2022-07-10 19:27:30
1875 010042501329E000 MONSTER HUNTER STORIES 2: WINGS OF RUIN Trial Version demo playable 2022-11-13 22:20:26
1876 0100C51003B46000 Monster Hunter XX Demo 32-bit;cpu nothing 2020-03-22 10:12:28
1877 0100C3800049C000 Monster Hunter XX Nintendo Switch Ver ( Double Cross ) playable 2024-07-21 14:08:09
1878 010088400366E000 Monster Jam Crush It! UE4;nvdec;online playable 2021-04-08 19:29:27
1879 010095C00F354000 Monster Jam Steel Titans crash;nvdec;UE4 menus 2021-11-14 09:45:38
1917 010035901046C000 Mushroom Quest playable 2020-05-17 13:07:08
1918 0100700006EF6000 Mushroom Wars 2 nvdec playable 2020-09-28 15:26:08
1919 010046400F310000 Music Racer playable 2020-08-10 08:51:23
1920 0100153006300000 Musou Orochi 2 Ultimate crash;nvdec boots 2021-04-09 19:39:16
1921 0100F6000EAA8000 Must Dash Amigos playable 2022-09-20 16:45:56
1922 01007B6006092000 MUSYNX playable 2020-05-08 14:24:43
1923 0100C3E00ACAA000 Mutant Football League: Dynasty Edition online-broken playable 2022-08-05 17:01:51
1948 0100A6F00AC70000 NAIRI: Tower of Shirin nvdec playable 2020-08-09 19:49:12
1949 010002F001220000 NAMCO MUSEUM ldn-untested playable 2024-08-13 07:52:21
1950 0100DAA00AEE6000 NAMCO MUSEUM™ ARCADE PAC™ playable 2021-06-07 21:44:50
1951 010039F010E14000 NAMCOT COLLECTION audio playable 2020-06-25 13:35:22
1952 010072B00BDDE000 Narcos: Rise of the Cartels UE4;crash;nvdec boots 2021-03-22 13:18:47
1953 01006BB00800A000 NARUTO SHIPPUDEN: Ultimate Ninja STORM 3 Full Burst nvdec playable 2024-06-16 14:58:05
1954 010084D00CF5E000 NARUTO SHIPPUDEN™: Ultimate Ninja® STORM 4 ROAD TO BORUTO playable 2024-06-29 13:04:22
2089 0100F9D00C186000 Olympia Soiree playable 2022-12-04 21:07:12
2090 0100A8B00E14A000 Olympic Games Tokyo 2020 – The Official Video Game™ ldn-untested;nvdec;online playable 2021-01-06 01:20:24
2091 01001D600E51A000 Omega Labyrinth Life playable 2021-02-23 21:03:03
2092 01005DE00CA34000 Omega Vampire nvdec playable 2020-10-17 19:15:35
2093 0100CDC00C40A000 Omensight: Definitive Edition UE4;crash;nvdec ingame 2020-07-26 01:45:14
2094 01006DB00D970000 OMG Zombies! 32-bit playable 2021-04-12 18:04:45
2095 010014E017B14000 OMORI playable 2023-01-07 20:21:02
2096 0100A5F011800000 Once Upon A Coma nvdec playable 2020-08-01 12:09:39
2097 0100BD3006A02000 One More Dungeon playable 2021-01-06 09:10:58
2098 010076600FD64000 One Person Story playable 2020-07-14 11:51:02
2099 0100774009CF6000 ONE PIECE Pirate Warriors 3 Deluxe Edition nvdec playable 2020-05-10 06:23:52
2184 010062B01525C000 Persona 4 Golden playable 2024-08-07 17:48:07
2185 01005CA01580E000 Persona 5 Royal gpu ingame 2024-08-17 21:45:15
2186 010087701B092000 Persona 5 Tactica playable 2024-04-01 22:21:03
2187 0100E4F010D92000 Persona 5: Scramble deadlock boots 2020-10-04 03:22:29
2188 0100801011C3E000 Persona® 5 Strikers nvdec;mac-bug playable 2023-09-26 09:36:01
2189 010044400EEAE000 Petoons Party nvdec playable 2021-03-02 21:07:58
2190 010053401147C000 PGA TOUR 2K21 deadlock;nvdec ingame 2022-10-05 21:53:50
2273 0100D1C01C194000 Powerful Pro Baseball 2024-2025 gpu ingame 2024-08-25 06:40:48
2274 01008E100E416000 PowerSlave Exhumed gpu ingame 2023-07-31 23:19:10
2275 010054F01266C000 Prehistoric Dude gpu ingame 2020-10-12 12:38:48
2276 0100DB200D3E4000 Pretty Princess Magical Coordinate playable 2020-10-15 11:43:41
2277 01007F00128CC000 Pretty Princess Party playable 2022-10-19 17:23:58
2278 010009300D278000 Preventive Strike nvdec playable 2022-10-06 10:55:51
2279 0100210019428000 Prince of Persia The Lost Crown crash ingame 2024-06-08 21:31:58
2294 0100ACE00DAB6000 Project Nimbus: Complete Edition nvdec;UE4;vulkan-backend-bug playable 2022-08-10 17:35:43
2295 01002980140F6000 Project TRIANGLE STRATEGY™ Debut Demo UE4;demo playable 2022-10-24 21:40:27
2296 0100BDB01150E000 Project Warlock playable 2020-06-16 10:50:41
2297 01009F100BC52000 Psikyo Collection Vol 1 32-bit playable 2020-10-11 13:18:47
2298 0100A2300DB78000 Psikyo Collection Vol. 3 ingame 2021-06-07 02:46:23
2299 01009D400C4A8000 Psikyo Collection Vol.2 32-bit playable 2021-06-07 03:22:07
2300 01007A200F2E2000 Psikyo Shooting Stars Alpha 32-bit playable 2021-04-13 12:03:43
2301 0100D7400F2E4000 Psikyo Shooting Stars Bravo 32-bit playable 2021-06-14 12:09:07
2302 0100EC100A790000 PSYVARIAR DELTA nvdec playable 2021-01-20 13:01:46
2303 0100AE700F184000 Puchitto kurasutā Need-Update;crash;services menus 2020-07-04 16:44:28
2304 0100D61010526000 Pulstario playable 2022-10-06 11:02:01
2305 01009AE00B788000 Pumped BMX Pro nvdec;online-broken playable 2022-09-20 17:40:50
2306 01006C10131F6000 Pumpkin Jack nvdec;UE4 playable 2022-10-13 12:52:32
2325 0100DCF00F13A000 Queen's Quest 4: Sacred Truce nvdec playable 2022-10-13 12:59:21
2326 0100492012378000 Quell gpu ingame 2021-06-11 15:59:53
2327 01001DE005012000 Quest of Dungeons playable 2021-06-07 10:29:22
2328 010067D011E68000 QuietMansion2 playable 2020-09-03 14:59:35
2329 0100AF100EE76000 Quiplash 2 InterLASHional: The Say Anything Party Game! online-working playable 2022-10-19 17:43:45
2330 0100E5400BE64000 R-Type Dimensions EX playable 2020-10-09 12:04:43
2331 0100F930136B6000 R-Type® Final 2 slow;nvdec;UE4 ingame 2022-10-30 21:46:29
2383 01007A800D520000 Refunct UE4 playable 2020-12-15 22:46:21
2384 0100FDF0083A6000 Regalia: Of Men and Monarchs - Royal Edition playable 2022-08-11 12:24:01
2385 01005FD00F15A000 Regions of Ruin playable 2020-08-05 11:38:58
2386 0100B5800C0E4000 Reine des Fleurs cpu;crash boots 2020-09-27 18:50:39
2387 0100F1900B144000 REKT! High Octane Stunts online playable 2020-09-28 12:33:56
2388 01002AD013C52000 Relicta nvdec;UE4 playable 2022-10-31 12:48:33
2389 010095900B436000 RemiLore playable 2021-06-03 18:58:15
2495 01002DF00F76C000 SAMURAI SHODOWN UE4;crash;nvdec menus 2020-09-06 02:17:00
2496 0100F6800F48E000 SAMURAI SHODOWN NEOGEO COLLECTION nvdec playable 2021-06-14 17:12:56
2497 0100B6501A360000 Samurai Warrior playable 2023-02-27 18:42:38
2498 01000EA00B23C000 Sangoku Rensenki ~Otome no Heihou!~ gpu;nvdec ingame 2020-10-17 19:13:14
2499 0100A4700BC98000 Satsujin Tantei Jack the Ripper playable 2021-06-21 16:32:54
2500 0100F0000869C000 Saturday Morning RPG nvdec playable 2022-08-12 12:41:50
2501 01006EE00380C000 Sausage Sports Club gpu ingame 2021-01-10 05:37:17
2532 010054400D2E6000 SEGA AGES Virtua Racing online-broken playable 2023-01-29 17:08:39
2533 01001E700AC60000 SEGA AGES Wonder Boy: Monster Land online playable 2021-05-05 16:28:25
2534 0100B3C014BDA000 SEGA Genesis™ – Nintendo Switch Online crash;regression nothing 2022-04-11 07:27:21
2535 0100F7300B24E000 SEGA Mega Drive Classics online playable 2021-01-05 11:08:00
2536 01009840046BC000 Semispheres playable 2021-01-06 23:08:31
2537 0100D1800D902000 SENRAN KAGURA Peach Ball playable 2021-06-03 15:12:10
2538 0100E0C00ADAC000 SENRAN KAGURA Reflexions playable 2020-03-23 19:15:23
2585 0100B2E00F13E000 Shipped playable 2020-11-21 14:22:32
2586 01000E800FCB4000 Ships playable 2021-06-11 16:14:37
2587 01007430122D0000 Shiren the Wanderer: The Tower of Fortune and the Dice of Fate nvdec playable 2022-10-20 11:44:36
2588 010027300A660000 Shiritsu Berubara Gakuen ~Versailles no Bara Re*imagination~ cpu;crash boots 2020-09-27 19:01:25
2589 01000244016BAE00 Shiro0 gpu ingame 2024-01-13 08:54:39
2590 0100CCE00DDB6000 Shoot 1UP DX playable 2020-12-13 12:32:47
2591 01001180021FA000 Shovel Knight: Specter of Torment playable 2020-05-30 08:34:17
2626 0100C52011460000 Sky: Children of the Light cpu;online-broken nothing 2023-02-23 10:57:10
2627 010041C01014E000 Skybolt Zack playable 2021-04-12 18:28:00
2628 0100A0A00D1AA000 SKYHILL playable 2021-03-05 15:19:11
2629 0100CCC0002E6000 Skylanders Imaginators crash;services boots 2020-05-30 18:49:18
2630 010021A00ABEE000 SKYPEACE playable 2020-05-29 14:14:30
2631 0100EA400BF44000 SkyScrappers playable 2020-05-28 22:11:25
2632 0100F3C00C400000 SkyTime slow ingame 2020-05-30 09:24:51
2681 01005EA01C0FC000 SONIC X SHADOW GENERATIONS crash ingame 2025-01-07 04:20:45
2682 010064F00C212000 Soul Axiom Rebooted nvdec;slow ingame 2020-09-04 12:41:01
2683 0100F2100F0B2000 Soul Searching playable 2020-07-09 18:39:07
2684 01008F2005154000 South Park™: The Fractured but Whole™ - Standard Edition slow;online-broken;vulkan-backend-bug;gpu slow;online-broken ingame playable 2025-01-21 17:35:10 2024-07-08 17:47:28
2685 0100B9F00C162000 Space Blaze playable 2020-08-30 16:18:05
2686 010005500E81E000 Space Cows UE4;crash menus 2020-06-15 11:33:20
2687 0100707011722000 Space Elite Force playable 2020-11-27 15:21:05
2797 0100681011B56000 Struggling playable 2020-10-15 20:37:03
2798 0100AF000B4AE000 Stunt Kite Party nvdec playable 2021-01-25 17:16:56
2799 0100C5500E7AE000 STURMWIND EX audio;32-bit playable 2022-09-16 12:01:39
2800 01001C1009892000 Subarashiki Kono Sekai -Final Remix- services;slow ingame 2020-02-10 16:21:51
2801 010001400E474000 Subdivision Infinity DX UE4;crash boots 2021-03-03 14:26:46
2802 0100E6400BCE8000 Sublevel Zero Redux playable 2022-09-16 12:30:03
2803 0100EDA00D866000 Submerged nvdec;UE4;vulkan-backend-bug playable 2022-08-16 15:17:01
2846 0100284007D6C000 Super One More Jump playable 2022-08-17 16:47:47
2847 01001F90122B2000 Super Punch Patrol playable 2024-07-12 19:49:02
2848 0100331005E8E000 Super Putty Squad gpu;32-bit ingame 2024-04-29 15:51:54
2849 01006C900CC60000 SUPER ROBOT WARS T online playable 2021-03-25 11:00:40
2850 0100CA400E300000 SUPER ROBOT WARS V online playable 2020-06-23 12:56:37
2851 010026800E304000 SUPER ROBOT WARS X online playable 2020-08-05 19:18:51
2852 01004CF00A60E000 Super Saurio Fly nvdec playable 2020-08-06 13:12:14
2853 010039700D200000 Super Skelemania playable 2020-06-07 22:59:50
2854 01006A800016E000 Super Smash Bros.™ Ultimate gpu;crash;nvdec;ldn-works;intel-vendor-bug ingame 2024-09-14 23:05:21
3014 010085A00C5E8000 The Lord of the Rings: Adventure Card Game - Definitive Edition online-broken menus 2022-09-16 15:19:32
3015 01008A000A404000 The Lost Child nvdec playable 2021-02-23 15:44:20
3016 0100BAB00A116000 The Low Road playable 2021-02-26 13:23:22
3017 01005F3006AFE000 The Mahjong Needs Update;crash;services nothing 2021-04-01 22:06:22
3018 0100DC300AC78000 The Messenger playable 2020-03-22 13:51:37
3019 0100DEC00B2BC000 The Midnight Sanctuary nvdec;UE4;vulkan-backend-bug playable 2022-10-03 17:17:32
3020 0100F1B00B456000 The MISSING: J.J. Macfield and the Island of Memories playable 2022-08-22 19:36:18
3060 010064E00ECBC000 The Unicorn Princess playable 2022-09-16 16:20:56
3061 0100BCF00E970000 The Vanishing of Ethan Carter UE4 playable 2021-06-09 17:14:47
3062 0100D0500B0A6000 The VideoKid nvdec playable 2021-01-06 09:28:24
3063 01008CF00BA38000 The Voice services menus 2020-07-28 20:48:49
3064 010056E00B4F4000 The Walking Dead: A New Frontier playable 2022-09-21 13:40:48
3065 010099100B6AC000 The Walking Dead: Season Two playable 2020-08-09 12:57:06
3066 010029200B6AA000 The Walking Dead: The Complete First Season playable 2021-06-04 13:10:56
3376 010022F00DA66000 Yooka-Laylee and the Impossible Lair playable 2021-03-05 17:32:21
3377 01006000040C2000 Yoshi’s Crafted World™ gpu;audout ingame 2021-08-30 13:25:51
3378 0100AE800C9C6000 Yoshi’s Crafted World™ Demo gpu boots 2020-12-16 14:57:40
3379 0100BBA00B23E000 Yoshiwara Higanbana Kuon no Chigiri nvdec playable 2020-10-17 19:14:46
3380 01003A400C3DA800 YouTube playable 2024-06-08 05:24:10
3381 00100A7700CCAA40 Youtubers Life00 nvdec playable 2022-09-03 14:56:19
3382 0100E390124D8000 Ys IX: Monstrum Nox playable 2022-06-12 04:14:42
3386 01002D60188DE000 Yu-Gi-Oh! Rush Duel: Dawn of the Battle Royale!! Let's Go! Go Rush!! crash ingame 2023-03-17 01:54:01
3387 010037D00DBDC000 YU-NO: A girl who chants love at the bound of this world. nvdec playable 2021-01-26 17:03:52
3388 0100B56011502000 Yumeutsutsu Re:After playable 2022-11-20 16:09:06
3389 0100DE200C0DA000 Yunohana Spring! - Mellow Times - audio;crash menus 2020-09-27 19:27:40
3390 0100307011C44000 Yuppie Psycho: Executive Edition crash ingame 2020-12-11 10:37:06
3391 0100FC900963E000 Yuri playable 2021-06-11 13:08:50
3392 010092400A678000 Zaccaria Pinball online-broken playable 2022-09-03 15:44:28
3397 0100AAC00E692000 Zenith playable 2022-09-17 09:57:02
3398 0100A6A00894C000 ZERO GUNNER 2- for Nintendo Switch playable 2021-01-04 20:17:14
3399 01004B001058C000 Zero Strain services;UE4 menus 2021-11-10 07:48:32
3400 010021300F69E000 Zettai kaikyu gakuen gpu;nvdec ingame 2020-08-25 15:15:54
3401 0100D7B013DD0000 Ziggy the Chaser playable 2021-02-04 20:34:27
3402 010086700EF16000 ZikSquare gpu ingame 2021-11-06 02:02:48
3403 010069C0123D8000 Zoids Wild Blast Unleashed nvdec playable 2022-10-15 11:26:59
3409 0100CD300A1BA000 Zombillie playable 2020-07-23 17:42:23
3410 01001EE00A6B0000 Zotrix: Solar Division playable 2021-06-07 20:34:05
3411 0100B9B00C6A4000 この世の果てで恋を唄う少女YU-NO audio ingame 2021-01-22 07:00:16
3412 0100E8600C504000 スーパーファミコン Nintendo Switch Online slow ingame 2020-03-14 05:48:38
3413 01000BB01CB8A000 トラブル・マギア ~訳アリ少女は未来を勝ち取るために異国の魔法学校へ留学します~(Trouble Magia ~Wakeari Shoujo wa Mirai o Kachitoru Tame ni Ikoku no Mahou Gakkou e Ryuugaku Shimasu~) nothing 2024-09-28 07:03:14
3414 010065500B218000 メモリーズオフ - Innocent Fille playable 2022-12-02 17:36:48
3415 010032400E700000 二ノ国 白き聖灰の女王 services;32-bit menus 2023-04-16 17:11:06

View File

@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" />
<ProjectReference Include="..\Ryujinx.SDL3.Common\Ryujinx.SDL3.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,16 @@
namespace Ryujinx.Audio.Backends.SDL3
{
class SDL3AudioBuffer
{
public readonly ulong DriverIdentifier;
public readonly ulong SampleCount;
public ulong SamplePlayed;
public SDL3AudioBuffer(ulong driverIdentifier, ulong sampleCount)
{
DriverIdentifier = driverIdentifier;
SampleCount = sampleCount;
SamplePlayed = 0;
}
}
}

View File

@@ -0,0 +1,190 @@
using Ryujinx.Audio.Common;
using Ryujinx.Audio.Integration;
using Ryujinx.Common.Logging;
using Ryujinx.Memory;
using Ryujinx.SDL3.Common;
using System;
using System.Collections.Concurrent;
using System.Threading;
using static Ryujinx.Audio.Integration.IHardwareDeviceDriver;
using static SDL3.SDL;
namespace Ryujinx.Audio.Backends.SDL3
{
public class SDL3HardwareDeviceDriver : IHardwareDeviceDriver
{
private readonly ManualResetEvent _updateRequiredEvent;
private readonly ManualResetEvent _pauseEvent;
private readonly ConcurrentDictionary<SDL3HardwareDeviceSession, byte> _sessions;
private readonly bool _supportSurroundConfiguration;
public float Volume { get; set; }
public SDL3HardwareDeviceDriver()
{
_updateRequiredEvent = new ManualResetEvent(false);
_pauseEvent = new ManualResetEvent(true);
_sessions = new ConcurrentDictionary<SDL3HardwareDeviceSession, byte>();
SDL3Driver.Instance.Initialize();
if (!SDL_GetAudioDeviceFormat(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, out var spec, out int _))
{
Logger.Error?.Print(LogClass.Application,
$"SDL_GetDefaultAudioInfo failed with error \"{SDL_GetError()}\"");
_supportSurroundConfiguration = true;
}
else
{
_supportSurroundConfiguration = spec.channels >= 6;
}
Volume = 1f;
}
public static bool IsSupported => IsSupportedInternal();
private static bool IsSupportedInternal()
{
nint device = OpenStream(SampleFormat.PcmInt16, Constants.TargetSampleRate, Constants.ChannelCountMax, null);
if (device != 0)
{
SDL_DestroyAudioStream(device);
}
return device != 0;
}
public ManualResetEvent GetUpdateRequiredEvent()
{
return _updateRequiredEvent;
}
public ManualResetEvent GetPauseEvent()
{
return _pauseEvent;
}
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager,
SampleFormat sampleFormat, uint sampleRate, uint channelCount)
{
if (channelCount == 0)
{
channelCount = 2;
}
if (sampleRate == 0)
{
sampleRate = Constants.TargetSampleRate;
}
if (direction != Direction.Output)
{
throw new NotImplementedException("Input direction is currently not implemented on SDL3 backend!");
}
SDL3HardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount);
_sessions.TryAdd(session, 0);
return session;
}
internal bool Unregister(SDL3HardwareDeviceSession session)
{
return _sessions.TryRemove(session, out _);
}
private static SDL_AudioSpec GetSDL3Spec(SampleFormat requestedSampleFormat, uint requestedSampleRate,
uint requestedChannelCount)
{
return new SDL_AudioSpec
{
channels = (byte)requestedChannelCount,
format = GetSDL3Format(requestedSampleFormat),
freq = (int)requestedSampleRate,
};
}
internal static SDL_AudioFormat GetSDL3Format(SampleFormat format)
{
return format switch
{
SampleFormat.PcmInt8 => SDL_AudioFormat.SDL_AUDIO_S8,
SampleFormat.PcmInt16 => SDL_AudioFormat.SDL_AUDIO_S16,
SampleFormat.PcmInt32 => SDL_AudioFormat.SDL_AUDIO_S32,
SampleFormat.PcmFloat => SDL_AudioFormat.SDL_AUDIO_F32,
_ => throw new ArgumentException($"Unsupported sample format {format}"),
};
}
internal static nint OpenStream(SampleFormat requestedSampleFormat, uint requestedSampleRate,
uint requestedChannelCount, SDL_AudioStreamCallback callback)
{
SDL_AudioSpec desired = GetSDL3Spec(requestedSampleFormat, requestedSampleRate, requestedChannelCount);
nint stream =
SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, ref desired, callback, nint.Zero);
if (stream == 0)
{
Logger.Error?.Print(LogClass.Application,
$"SDL3 open audio device initialization failed with error \"{SDL_GetError()}\"");
return 0;
}
return stream;
}
public void Dispose()
{
GC.SuppressFinalize(this);
Dispose(true);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
foreach (SDL3HardwareDeviceSession session in _sessions.Keys)
{
session.Dispose();
}
SDL3Driver.Instance.Dispose();
_pauseEvent.Dispose();
}
}
public bool SupportsSampleRate(uint sampleRate)
{
return true;
}
public bool SupportsSampleFormat(SampleFormat sampleFormat)
{
return sampleFormat != SampleFormat.PcmInt24;
}
public bool SupportsChannelCount(uint channelCount)
{
if (channelCount == 6)
{
return _supportSurroundConfiguration;
}
return true;
}
public bool SupportsDirection(Direction direction)
{
// TODO: add direction input when supported.
return direction == Direction.Output;
}
}
}

View File

@@ -0,0 +1,231 @@
using Ryujinx.Audio.Backends.Common;
using Ryujinx.Audio.Common;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Memory;
using Ryujinx.Memory;
using System;
using System.Collections.Concurrent;
using System.Threading;
using static SDL3.SDL;
namespace Ryujinx.Audio.Backends.SDL3
{
class SDL3HardwareDeviceSession : HardwareDeviceSessionOutputBase
{
private readonly SDL3HardwareDeviceDriver _driver;
private readonly ConcurrentQueue<SDL3AudioBuffer> _queuedBuffers;
private readonly DynamicRingBuffer _ringBuffer;
private ulong _playedSampleCount;
private readonly ManualResetEvent _updateRequiredEvent;
private nint _outputStream;
private bool _hasSetupError;
private readonly SDL_AudioStreamCallback _callbackDelegate;
private readonly int _bytesPerFrame;
private bool _started;
private float _volume;
private readonly SDL_AudioFormat _nativeSampleFormat;
public SDL3HardwareDeviceSession(SDL3HardwareDeviceDriver driver, IVirtualMemoryManager memoryManager,
SampleFormat requestedSampleFormat, uint requestedSampleRate, uint requestedChannelCount) : base(
memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
{
_driver = driver;
_updateRequiredEvent = _driver.GetUpdateRequiredEvent();
_queuedBuffers = new ConcurrentQueue<SDL3AudioBuffer>();
_ringBuffer = new DynamicRingBuffer();
_callbackDelegate = Update;
_bytesPerFrame = BackendHelper.GetSampleSize(RequestedSampleFormat) * (int)RequestedChannelCount;
_nativeSampleFormat = SDL3HardwareDeviceDriver.GetSDL3Format(RequestedSampleFormat);
_started = false;
_volume = 1f;
}
private void EnsureAudioStreamSetup(AudioBuffer buffer)
{
uint bufferSampleCount = (uint)GetSampleCount(buffer);
bool needAudioSetup = (_outputStream == 0 && !_hasSetupError) ||
(bufferSampleCount >= Constants.TargetSampleCount);
if (needAudioSetup)
{
nint newOutputStream = SDL3HardwareDeviceDriver.OpenStream(RequestedSampleFormat, RequestedSampleRate,
RequestedChannelCount, _callbackDelegate);
_hasSetupError = newOutputStream == 0;
if (!_hasSetupError)
{
if (_outputStream != 0)
{
SDL_DestroyAudioStream(_outputStream);
}
_outputStream = newOutputStream;
if (_started)
{
SDL_ResumeAudioStreamDevice(_outputStream);
}
else
{
SDL_PauseAudioStreamDevice(_outputStream);
}
}
}
}
private unsafe void Update(nint userdata, nint stream, int additionalAmount, int totalAmount)
{
int maxFrameCount = (int)GetSampleCount(additionalAmount);
int bufferedFrames = _ringBuffer.Length / _bytesPerFrame;
int frameCount = Math.Min(bufferedFrames, maxFrameCount);
if (frameCount == 0)
{
return;
}
using SpanOwner<byte> samplesOwner = SpanOwner<byte>.Rent(frameCount * _bytesPerFrame);
Span<byte> samples = samplesOwner.Span;
int samplesLength = samples.Length;
_ringBuffer.Read(samples, 0, samplesLength);
fixed (byte* p = samples)
{
nint pStreamSrc = (nint)p;
nint pStreamDst = SDL_calloc(1,samplesLength);
// Apply volume to written data
SDL_MixAudio(pStreamDst, pStreamSrc, _nativeSampleFormat, (uint)samplesLength, _driver.Volume);
SDL_PutAudioStreamData(stream, pStreamDst, samplesLength);
SDL_free(pStreamDst);
}
ulong sampleCount = GetSampleCount(samplesLength);
ulong availaibleSampleCount = sampleCount;
bool needUpdate = false;
while (availaibleSampleCount > 0 && _queuedBuffers.TryPeek(out SDL3AudioBuffer driverBuffer))
{
ulong sampleStillNeeded = driverBuffer.SampleCount - Interlocked.Read(ref driverBuffer.SamplePlayed);
ulong playedAudioBufferSampleCount = Math.Min(sampleStillNeeded, availaibleSampleCount);
ulong currentSamplePlayed =
Interlocked.Add(ref driverBuffer.SamplePlayed, playedAudioBufferSampleCount);
availaibleSampleCount -= playedAudioBufferSampleCount;
if (currentSamplePlayed == driverBuffer.SampleCount)
{
_queuedBuffers.TryDequeue(out _);
needUpdate = true;
}
Interlocked.Add(ref _playedSampleCount, playedAudioBufferSampleCount);
}
// Notify the output if needed.
if (needUpdate)
{
_updateRequiredEvent.Set();
}
}
public override ulong GetPlayedSampleCount()
{
return Interlocked.Read(ref _playedSampleCount);
}
public override float GetVolume()
{
return _volume;
}
public override void PrepareToClose() { }
public override void QueueBuffer(AudioBuffer buffer)
{
EnsureAudioStreamSetup(buffer);
if (_outputStream != 0)
{
SDL3AudioBuffer driverBuffer = new(buffer.DataPointer, GetSampleCount(buffer));
_ringBuffer.Write(buffer.Data, 0, buffer.Data.Length);
_queuedBuffers.Enqueue(driverBuffer);
}
else
{
Interlocked.Add(ref _playedSampleCount, GetSampleCount(buffer));
_updateRequiredEvent.Set();
}
}
public override void SetVolume(float volume)
{
_volume = volume;
}
public override void Start()
{
if (!_started)
{
if (_outputStream != 0)
{
SDL_ResumeAudioStreamDevice(_outputStream);
}
_started = true;
}
}
public override void Stop()
{
if (_started)
{
if (_outputStream != 0)
{
SDL_PauseAudioStreamDevice(_outputStream);
}
_started = false;
}
}
public override void UnregisterBuffer(AudioBuffer buffer) { }
public override bool WasBufferFullyConsumed(AudioBuffer buffer)
{
if (!_queuedBuffers.TryPeek(out SDL3AudioBuffer driverBuffer))
{
return true;
}
return driverBuffer.DriverIdentifier != buffer.DataPointer;
}
protected virtual void Dispose(bool disposing)
{
if (disposing && _driver.Unregister(this))
{
PrepareToClose();
Stop();
if (_outputStream != 0)
{
SDL_DestroyAudioStream(_outputStream);
}
}
}
public override void Dispose()
{
Dispose(true);
}
}
}

View File

@@ -78,10 +78,5 @@ namespace Ryujinx.Common.Configuration.Hid.Controller
/// Controller Rumble Settings
/// </summary>
public RumbleConfigController Rumble { get; set; }
/// <summary>
/// Controller LED Settings
/// </summary>
public LedConfigController Led { get; set; }
}
}

View File

@@ -1,25 +0,0 @@
namespace Ryujinx.Common.Configuration.Hid.Controller
{
public class LedConfigController
{
/// <summary>
/// Enable LED color changing by the emulator
/// </summary>
public bool EnableLed { get; set; }
/// <summary>
/// Ignores the color and disables the LED entirely.
/// </summary>
public bool TurnOffLed { get; set; }
/// <summary>
/// Ignores the color and uses the rainbow color functionality for the LED.
/// </summary>
public bool UseRainbow { get; set; }
/// <summary>
/// Packed RGB int of the color
/// </summary>
public uint LedColor { get; set; }
}
}

View File

@@ -8,6 +8,6 @@ namespace Ryujinx.Common.Configuration.Hid
{
Invalid,
WindowKeyboard,
GamepadSDL2,
GamepadSDL3
}
}

View File

@@ -58,7 +58,7 @@ namespace Ryujinx.Common.Configuration.Hid
return backendType switch
{
InputBackendType.WindowKeyboard => JsonSerializer.Deserialize(ref reader, _serializerContext.StandardKeyboardInputConfig),
InputBackendType.GamepadSDL2 => JsonSerializer.Deserialize(ref reader, _serializerContext.StandardControllerInputConfig),
InputBackendType.GamepadSDL3 => JsonSerializer.Deserialize(ref reader, _serializerContext.StandardControllerInputConfig),
_ => throw new InvalidOperationException($"Unknown backend type {backendType}"),
};
}
@@ -70,7 +70,7 @@ namespace Ryujinx.Common.Configuration.Hid
case InputBackendType.WindowKeyboard:
JsonSerializer.Serialize(writer, value as StandardKeyboardInputConfig, _serializerContext.StandardKeyboardInputConfig);
break;
case InputBackendType.GamepadSDL2:
case InputBackendType.GamepadSDL3:
JsonSerializer.Serialize(writer, value as StandardControllerInputConfig, _serializerContext.StandardControllerInputConfig);
break;
default:

View File

@@ -1,23 +0,0 @@
using System;
using System.Runtime.InteropServices;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable InconsistentNaming
namespace Ryujinx.Common.Helper
{
public static class RunningPlatform
{
public static bool IsMacOS => OperatingSystem.IsMacOS();
public static bool IsWindows => OperatingSystem.IsWindows();
public static bool IsLinux => OperatingSystem.IsLinux();
public static bool IsIntelMac => IsMacOS && RuntimeInformation.OSArchitecture is Architecture.X64;
public static bool IsArmMac => IsMacOS && RuntimeInformation.OSArchitecture is Architecture.Arm64;
public static bool IsX64Windows => IsWindows && (RuntimeInformation.OSArchitecture is Architecture.X64);
public static bool IsArmWindows => IsWindows && (RuntimeInformation.OSArchitecture is Architecture.Arm64);
public static bool IsX64Linux => IsLinux && (RuntimeInformation.OSArchitecture is Architecture.X64);
public static bool IsArmLinux => IsLinux && (RuntimeInformation.OSArchitecture is Architecture.Arm64);
}
}

View File

@@ -1,6 +1,5 @@
using Gommon;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Helper;
using System;
using System.Linq;
using System.Runtime.InteropServices;
@@ -9,20 +8,19 @@ namespace Ryujinx.Common
{
public static class TitleIDs
{
public static ReactiveObject<Optional<string>> CurrentApplication { get; } = new();
public static ReactiveObject<Optional<string>> CurrentApplication { get; set; } = new();
public static GraphicsBackend SelectGraphicsBackend(string titleId, GraphicsBackend currentBackend)
{
switch (currentBackend)
{
case GraphicsBackend.Metal when !OperatingSystem.IsMacOS():
case GraphicsBackend.OpenGl when OperatingSystem.IsMacOS():
return GraphicsBackend.Vulkan;
case GraphicsBackend.Vulkan or GraphicsBackend.OpenGl or GraphicsBackend.Metal:
return currentBackend;
}
if (!RunningPlatform.IsArmMac)
if (!(OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture is Architecture.Arm64))
return GraphicsBackend.Vulkan;
return GreatMetalTitles.ContainsIgnoreCase(titleId) ? GraphicsBackend.Metal : GraphicsBackend.Vulkan;
@@ -30,28 +28,18 @@ namespace Ryujinx.Common
public static readonly string[] GreatMetalTitles =
[
"010076f0049a2000", // Bayonetta
"01006f8002326000", // Animal Crossings: New Horizons
"01009bf0072d4000", // Captain Toad: Treasure Tracker
"0100a5c00d162000", // Cuphead
"010023800d64a000", // Deltarune
"01003a30012c0000", // LEGO City Undercover
"010028600EBDA000", // Mario 3D World
"0100152000022000", // Mario Kart 8 Deluxe
"010075a016a3a000", // Persona 4 Arena Ultimax
"0100187003A36000", // Pokémon: Let's Go, Eevee!
"01005CA01580E000", // Persona 5
"0100187003A36000", // Pokémon: Let's Go, Evoli!
"010003f003a34000", // Pokémon: Let's Go, Pikachu!
"01008C0016544000", // Sea of Stars
"01006A800016E000", // Smash Ultimate
"01006bb00c6f0000", // The Legend of Zelda: Link's Awakening
// These ones have small issues, but those happen on Vulkan as well:
"01006f8002326000", // Animal Crossings: New Horizons
"01009bf0072d4000", // Captain Toad: Treasure Tracker
"01009510001ca000", // Fast RMX
"01005CA01580E000", // Persona 5 Royale
"0100000000010000", // Super Mario Odyssey
//Isaac claims it has a issue in level 2, but I am not able to replicate it on my M3. More testing would be appreciated:
"010015100b514000", // Super Mario Bros. Wonder
"0100000000010000", // Super Mario Odyessy
];
public static string GetDiscordGameAsset(string titleId)
@@ -59,87 +47,72 @@ namespace Ryujinx.Common
public static readonly string[] DiscordGameAssetKeys =
[
//All games are in Alphabetical order by Game name.
//Dragon Quest Franchise
"010008900705c000", // Dragon Quest Builders
"010042000a986000", // Dragon Quest Builders 2
//Fire Emblem Franchise
"0100a6301214e000", // Fire Emblem Engage
"0100a12011cc8000", // Fire Emblem: Shadow Dragon
"010055d009f78000", // Fire Emblem: Three Houses
"0100a12011cc8000", // Fire Emblem: Shadow Dragon
"0100a6301214e000", // Fire Emblem Engage
"0100f15003e64000", // Fire Emblem Warriors
"010071f0143ea000", // Fire Emblem Warriors: Three Hopes
//Kirby Franchise
"01004d300c5ae000", // Kirby and the Forgotten Land
"0100a8e016236000", // Kirby's Dream Buffet
"0100227010460000", // Kirby Fighters 2
"01006b601380e000", // Kirby's Return to Dream Land Deluxe
"01007e3006dda000", // Kirby Star Allies
"01004d300c5ae000", // Kirby and the Forgotten Land
"01006b601380e000", // Kirby's Return to Dream Land Deluxe
"01003fb00c5a8000", // Super Kirby Clash
//The Zelda Franchise
"01000b900d8b0000", // Cadence of Hyrule
"0100ae00096ea000", // Hyrule Warriors: Definitive Edition
"01002b00111a2000", // Hyrule Warriors: Age of Calamity
"0100227010460000", // Kirby Fighters 2
"0100a8e016236000", // Kirby's Dream Buffet
"01007ef00011e000", // The Legend of Zelda: Breath of the Wild
"01006bb00c6f0000", // The Legend of Zelda: Link's Awakening
"01002da013484000", // The Legend of Zelda: Skyward Sword HD
"0100f2c0115b6000", // The Legend of Zelda: Tears of the Kingdom
"01008cf01baac000", // The Legend of Zelda: Echoes of Wisdom
"01000b900d8b0000", // Cadence of Hyrule
"0100ae00096ea000", // Hyrule Warriors: Definitive Edition
"01002b00111a2000", // Hyrule Warriors: Age of Calamity
//Luigi Franchise
"010048701995e000", // Luigi's Mansion 2 HD
"0100dca0064a6000", // Luigi's Mansion 3
//Metroid Franchise
"010093801237c000", // Metroid Dread
"010012101468c000", // Metroid Prime Remastered
//Monster Hunter Franchise
"0100770008dd8000", // Monster Hunter Generations Ultimate
"0100b04011742000", // Monster Hunter Rise
//Mario Franchise
"0100000000010000", // SUPER MARIO ODYSSEY
"0100ea80032ea000", // Super Mario Bros. U Deluxe
"01009b90006dc000", // Super Mario Maker 2
"010049900f546000", // Super Mario 3D All-Stars
"010049900F546001", // ^ 64
"010049900F546002", // ^ Sunshine
"010049900F546003", // ^ Galaxy
"010028600ebda000", // Super Mario 3D World + Bowser's Fury
"010015100b514000", // Super Mario Bros. Wonder
"0100152000022000", // Mario Kart 8 Deluxe
"010036b0034e4000", // Super Mario Party
"01006fe013472000", // Mario Party Superstars
"0100965017338000", // Super Mario Party Jamboree
"01006d0017f7a000", // Mario & Luigi: Brothership
"010003000e146000", // Mario & Sonic at the Olympic Games Tokyo 2020
"010067300059a000", // Mario + Rabbids: Kingdom Battle
"0100317013770000", // Mario + Rabbids: Sparks of Hope
"0100c9c00e25c000", // Mario Golf: Super Rush
"0100152000022000", // Mario Kart 8 Deluxe
"01006fe013472000", // Mario Party Superstars
"010019401051c000", // Mario Strikers: Battle League
"0100bde00862a000", // Mario Tennis Aces
"0100b99019412000", // Mario vs. Donkey Kong
"010049900f546000", // Super Mario 3D All-Stars
"010028600ebda000", // Super Mario 3D World + Bowser's Fury
"010049900F546001", // Super Mario 64
"0100ea80032ea000", // Super Mario Bros. U Deluxe
"010015100b514000", // Super Mario Bros. Wonder
"010049900F546003", // Super Mario Galaxy
"01009b90006dc000", // Super Mario Maker 2
"0100000000010000", // SUPER MARIO ODYSSEY
"010036b0034e4000", // Super Mario Party
"0100965017338000", // Super Mario Party Jamboree
"0100bc0018138000", // Super Mario RPG
"010049900F546002", // Super Mario Sunshine
"0100a3900c3e2000", // Paper Mario: The Origami King
"0100ecd018ebe000", // Paper Mario: The Thousand-Year Door
"0100bc0018138000", // Super Mario RPG
"0100bde00862a000", // Mario Tennis Aces
"0100c9c00e25c000", // Mario Golf: Super Rush
"010019401051c000", // Mario Strikers: Battle League
"010003000e146000", // Mario & Sonic at the Olympic Games Tokyo 2020
"0100b99019412000", // Mario vs. Donkey Kong
//Pikmin Franchise
"0100aa80194b0000", // Pikmin 1
"0100d680194b2000", // Pikmin 2
"0100f4c009322000", // Pikmin 3 Deluxe
"0100b7c00933a000", // Pikmin 4
//The Pokémon Franchise
"0100f4300bf2c000", // New Pokémon Snap
"0100000011d90000", // Pokémon Brilliant Diamond
"01001f5010dfa000", // Pokémon Legends: Arceus
"010003f003a34000", // Pokémon: Let's Go Pikachu!
"0100187003a36000", // Pokémon: Let's Go Eevee!
"01003d200baa2000", // Pokémon Mystery Dungeon - Rescue Team DX
"0100a3d008c5c000", // Pokémon Scarlet
"01008db008c2c000", // Pokémon Shield
@@ -147,29 +120,24 @@ namespace Ryujinx.Common
"0100abf008968000", // Pokémon Sword
"01008f6008c5e000", // Pokémon Violet
"0100b3f000be2000", // Pokkén Tournament DX
"0100187003a36000", // Pokémon: Let's Go Eevee!
"010003f003a34000", // Pokémon: Let's Go Pikachu!
//Splatoon Franchise
"01003bc0000a0000", // Splatoon 2 (US)
"0100f8f0000a2000", // Splatoon 2 (EU)
"01003c700009c000", // Splatoon 2 (JP)
"01003bc0000a0000", // Splatoon 2 (US)
"0100c2500fc20000", // Splatoon 3
"0100ba0018500000", // Splatoon 3: Splatfest World Premiere
//NSO Membership games
"010040600c5ce000", // Tetris 99
"0100277011f1a000", // Super Mario Bros. 35
"0100ad9012510000", // PAC-MAN 99
"0100ccf019c8c000", // F-ZERO 99
"0100c62011050000", // GB - Nintendo Switch Online
"010012f017576000", // GBA - Nintendo Switch Online
"0100d870045b6000", // NES - Nintendo Switch Online
"01008d300c50c000", // SNES - Nintendo Switch Online
"0100c9a00ece6000", // N64 - Nintendo Switch Online
"0100e0601c632000", // N64 - Nintendo Switch Online 18+
"0100d870045b6000", // NES - Nintendo Switch Online
"0100ad9012510000", // PAC-MAN 99
"010040600c5ce000", // Tetris 99
"01008d300c50c000", // SNES - Nintendo Switch Online
"0100277011f1a000", // Super Mario Bros. 35
"0100c62011050000", // GB - Nintendo Switch Online
"010012f017576000", // GBA - Nintendo Switch Online
//Misc Nintendo 1st party games
"01000320000cc000", // 1-2 Switch
"0100300012f2a000", // Advance Wars 1+2: Re-Boot Camp
"01006f8002326000", // Animal Crossing: New Horizons
@@ -184,32 +152,27 @@ namespace Ryujinx.Common
"01006a800016e000", // Super Smash Bros. Ultimate
"0100a9400c9c2000", // Tokyo Mirage Sessions #FE Encore
//Bayonetta Franchise
"010076f0049a2000", // Bayonetta
"01007960049a0000", // Bayonetta 2
"01004a4010fea000", // Bayonetta 3
"0100cf5010fec000", // Bayonetta Origins: Cereza and the Lost Demon
//Persona Franchise
"0100dcd01525a000", // Persona 3 Portable
"010075a016a3a000", // Persona 4 Arena Ultimax
"010062b01525c000", // Persona 4 Golden
"010075a016a3a000", // Persona 4 Arena Ultimax
"01005ca01580e000", // Persona 5 Royal
"0100801011c3e000", // Persona 5 Strikers
"010087701b092000", // Persona 5 Tactica
//Sonic Franchise
"01004ad014bf0000", // Sonic Frontiers
"01009aa000faa000", // Sonic Mania
"01004ad014bf0000", // Sonic Frontiers
"01005ea01c0fc000", // SONIC X SHADOW GENERATIONS
"01005ea01c0fc001", // ^
//Xenoblade Franchise
"0100ff500e34a000", // Xenoblade Chronicles - Definitive Edition
"0100e95004038000", // Xenoblade Chronicles 2
"010074f013262000", // Xenoblade Chronicles 3
//Misc Games
"010056e00853a000", // A Hat in Time
"0100fd1014726000", // Baldurs Gate: Dark Alliance
"0100dbf01000a000", // Burnout Paradise Remastered
@@ -222,6 +185,8 @@ namespace Ryujinx.Common
"01003620068ea000", // Hand of Fate 2
"010085500130a000", // Lego City: Undercover
"010073c01af34000", // LEGO Horizon Adventures
"0100770008dd8000", // Monster Hunter Generations Ultimate
"0100b04011742000", // Monster Hunter Rise
"0100853015e86000", // No Man's Sky
"01007bb017812000", // Portal
"0100abd01785c000", // Portal 2

View File

@@ -1,88 +0,0 @@
using System;
using System.Drawing;
namespace Ryujinx.Common.Utilities
{
public class Rainbow
{
public static float Speed { get; set; } = 1;
public static Color Color { get; private set; } = Color.Blue;
private static float _lastHue;
public static void Tick()
{
float currentHue = Color.GetHue();
float nextHue = currentHue;
if (currentHue >= 360)
nextHue = 0;
else
nextHue += Speed;
Color = HsbToRgb(
nextHue / 360,
1,
1
);
_lastHue = currentHue;
RainbowColorUpdated?.Invoke(Color.ToArgb());
}
public static event Action<int> RainbowColorUpdated;
private static Color HsbToRgb(float hue, float saturation, float brightness)
{
int r = 0, g = 0, b = 0;
if (saturation == 0)
{
r = g = b = (int)(brightness * 255.0f + 0.5f);
}
else
{
float h = (hue - (float)Math.Floor(hue)) * 6.0f;
float f = h - (float)Math.Floor(h);
float p = brightness * (1.0f - saturation);
float q = brightness * (1.0f - saturation * f);
float t = brightness * (1.0f - (saturation * (1.0f - f)));
switch ((int)h)
{
case 0:
r = (int)(brightness * 255.0f + 0.5f);
g = (int)(t * 255.0f + 0.5f);
b = (int)(p * 255.0f + 0.5f);
break;
case 1:
r = (int)(q * 255.0f + 0.5f);
g = (int)(brightness * 255.0f + 0.5f);
b = (int)(p * 255.0f + 0.5f);
break;
case 2:
r = (int)(p * 255.0f + 0.5f);
g = (int)(brightness * 255.0f + 0.5f);
b = (int)(t * 255.0f + 0.5f);
break;
case 3:
r = (int)(p * 255.0f + 0.5f);
g = (int)(q * 255.0f + 0.5f);
b = (int)(brightness * 255.0f + 0.5f);
break;
case 4:
r = (int)(t * 255.0f + 0.5f);
g = (int)(p * 255.0f + 0.5f);
b = (int)(brightness * 255.0f + 0.5f);
break;
case 5:
r = (int)(brightness * 255.0f + 0.5f);
g = (int)(p * 255.0f + 0.5f);
b = (int)(q * 255.0f + 0.5f);
break;
}
}
return Color.FromArgb(Convert.ToByte(255), Convert.ToByte(r), Convert.ToByte(g), Convert.ToByte(b));
}
}
}

View File

@@ -118,7 +118,7 @@ namespace Ryujinx.Graphics.Gpu.Shader
private static string GetDiskCachePath()
{
return GraphicsConfig.EnableShaderCache && GraphicsConfig.TitleId != null
? Path.Combine(AppDataManager.GamesDirPath, GraphicsConfig.TitleId.ToLower(), "cache", "shader")
? Path.Combine(AppDataManager.GamesDirPath, GraphicsConfig.TitleId, "cache", "shader")
: null;
}

View File

@@ -710,8 +710,9 @@ namespace Ryujinx.HLE.FileSystem
{
updateNcasItem.Add((nca.Header.ContentType, entry.FullName));
}
else if (updateNcas.TryAdd(nca.Header.TitleId, new List<(NcaContentType, string)>()))
else
{
updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>());
updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullName));
}
}
@@ -897,8 +898,9 @@ namespace Ryujinx.HLE.FileSystem
{
updateNcasItem.Add((nca.Header.ContentType, entry.FullPath));
}
else if (updateNcas.TryAdd(nca.Header.TitleId, new List<(NcaContentType, string)>()))
else
{
updateNcas.Add(nca.Header.TitleId, new List<(NcaContentType, string)>());
updateNcas[nca.Header.TitleId].Add((nca.Header.ContentType, entry.FullPath));
}

View File

@@ -56,7 +56,7 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
ProfilesJson profilesJson = JsonHelper.DeserializeFromFile(_profilesJsonPath, _serializerContext.ProfilesJson);
return profilesJson.Profiles
.FindFirst(profile => profile.UserId == profilesJson.LastOpened)
.FindFirst(profile => profile.AccountState == AccountState.Open)
.Convert(profileJson => new UserProfile(new UserId(profileJson.UserId), profileJson.Name,
profileJson.Image, profileJson.LastModifiedTimestamp));
}

View File

@@ -659,7 +659,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
if (string.IsNullOrWhiteSpace(filePath))
{
throw new InvalidSystemResourceException("JIT (010000000000003B) system title not found! The JIT will not work, provide the system archive to fix this error. (See https://github.com/Ryubing/Ryujinx#requirements for more information)");
throw new InvalidSystemResourceException("JIT (010000000000003B) system title not found! The JIT will not work, provide the system archive to fix this error. (See https://github.com/GreemDev/Ryujinx#requirements for more information)");
}
context.Device.LoadNca(filePath);

View File

@@ -105,7 +105,7 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pl
titleName = "Unknown";
}
throw new InvalidSystemResourceException($"{titleName} ({fontTitle:x8}) system title not found! This font will not work, provide the system archive to fix this error. (See https://github.com/Ryubing/Ryujinx#requirements for more information)");
throw new InvalidSystemResourceException($"{titleName} ({fontTitle:x8}) system title not found! This font will not work, provide the system archive to fix this error. (See https://github.com/GreemDev/Ryujinx#requirements for more information)");
}
}
else

View File

@@ -244,15 +244,6 @@ namespace Ryujinx.HLE.HOS.Services.Settings
return ResultCode.Success;
}
[CommandCmif(68)]
// GetSerialNumber() -> buffer<nn::settings::system::SerialNumber, 0x16>
public ResultCode GetSerialNumber(ServiceCtx context)
{
context.ResponseData.Write(Encoding.ASCII.GetBytes("RYU00000000000"));
return ResultCode.Success;
}
[CommandCmif(77)]
// GetDeviceNickName() -> buffer<nn::settings::system::DeviceNickName, 0x16>
public ResultCode GetDeviceNickName(ServiceCtx context)

View File

@@ -51,9 +51,6 @@ namespace Ryujinx.HLE.HOS.Services.Spl
context.ResponseData.Write(configValue);
if (result == SmcResult.Success)
return ResultCode.Success;
return (ResultCode)((int)result << 9) | ResultCode.ModuleId;
}

View File

@@ -161,20 +161,5 @@ namespace Ryujinx.HLE
{
return 1000 / _frameRate[FrameTypeGame];
}
public string FormatGameFrameRate()
{
double frameRate = GetGameFrameRate();
double frameTime = GetGameFrameTime();
return $"{frameRate:00.00} FPS ({frameTime:00.00}ms)";
}
public string FormatFifoPercent()
{
double fifoPercent = GetFifoPercent();
return $"FIFO: {fifoPercent:00.00}%";
}
}
}

View File

@@ -34,8 +34,8 @@ namespace Ryujinx.HLE
public int CpuCoresCount = 4; //Switch 1 has 4 cores
public VSyncMode VSyncMode { get; set; }
public bool CustomVSyncIntervalEnabled { 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;

View File

@@ -1,9 +1,6 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using Ryujinx.HLE.HOS.Services.Hid;
using SDL2;
using System;
using System.Collections.Generic;
using System.Numerics;
@@ -88,7 +85,7 @@ namespace Ryujinx.Input.SDL2
Id = driverId;
Features = GetFeaturesFlag();
_triggerThreshold = 0.0f;
// Enable motion tracking
if (Features.HasFlag(GamepadFeaturesFlag.Motion))
{
@@ -104,18 +101,6 @@ namespace Ryujinx.Input.SDL2
}
}
public void SetLed(uint packedRgb)
{
if (!Features.HasFlag(GamepadFeaturesFlag.Led)) return;
byte red = packedRgb > 0 ? (byte)(packedRgb >> 16) : (byte)0;
byte green = packedRgb > 0 ? (byte)(packedRgb >> 8) : (byte)0;
byte blue = packedRgb > 0 ? (byte)(packedRgb % 256) : (byte)0;
if (SDL_GameControllerSetLED(_gamepadHandle, red, green, blue) != 0)
Logger.Error?.Print(LogClass.Hid, "LED is not supported on this game controller.");
}
private GamepadFeaturesFlag GetFeaturesFlag()
{
GamepadFeaturesFlag result = GamepadFeaturesFlag.None;
@@ -126,16 +111,13 @@ namespace Ryujinx.Input.SDL2
result |= GamepadFeaturesFlag.Motion;
}
if (SDL_GameControllerHasRumble(_gamepadHandle) == SDL_bool.SDL_TRUE)
int error = SDL_GameControllerRumble(_gamepadHandle, 0, 0, 100);
if (error == 0)
{
result |= GamepadFeaturesFlag.Rumble;
}
if (SDL_GameControllerHasLED(_gamepadHandle) == SDL_bool.SDL_TRUE)
{
result |= GamepadFeaturesFlag.Led;
}
return result;
}
@@ -226,32 +208,12 @@ namespace Ryujinx.Input.SDL2
private static Vector3 GsToMs2(Vector3 gs) => gs / SDL_STANDARD_GRAVITY;
private void RainbowColorChanged(int packedRgb)
{
if (!_configuration.Led.UseRainbow) return;
SetLed((uint)packedRgb);
}
public void SetConfiguration(InputConfig configuration)
{
lock (_userMappingLock)
{
_configuration = (StandardControllerInputConfig)configuration;
if (Features.HasFlag(GamepadFeaturesFlag.Led) && _configuration.Led.EnableLed)
{
if (_configuration.Led.TurnOffLed)
(this as IGamepad).ClearLed();
else if (_configuration.Led.UseRainbow)
Rainbow.RainbowColorUpdated += RainbowColorChanged;
else
SetLed(_configuration.Led.LedColor);
if (!_configuration.Led.UseRainbow)
Rainbow.RainbowColorUpdated -= RainbowColorChanged;
}
_buttonsUserMapping.Clear();
// First update sticks

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,24 +202,17 @@ namespace Ryujinx.Input.SDL2
}
nint gamepadHandle = SDL_GameControllerOpen(joystickIndex);
if (gamepadHandle == nint.Zero)
{
return null;
}
return new SDL2Gamepad(gamepadHandle, id);
}
public IEnumerable<IGamepad> GetGamepads()
{
lock (_gamepadsIds)
if (SDL_GameControllerName(gamepadHandle).StartsWith(SDL2JoyCon.Prefix))
{
foreach (string gamepadId in _gamepadsIds)
{
yield return GetGamepad(gamepadId);
}
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

@@ -1,6 +1,5 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Generic;
using System.Numerics;
@@ -386,11 +385,6 @@ namespace Ryujinx.Input.SDL2
}
}
public void SetLed(uint packedRgb)
{
Logger.Info?.Print(LogClass.UI, "SetLed called on an SDL2Keyboard");
}
public void SetTriggerThreshold(float triggerThreshold)
{
// No operations

View File

@@ -1,5 +1,4 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging;
using System;
using System.Drawing;
using System.Numerics;
@@ -77,11 +76,6 @@ namespace Ryujinx.Input.SDL2
throw new NotImplementedException();
}
public void SetLed(uint packedRgb)
{
Logger.Info?.Print(LogClass.UI, "SetLed called on an SDL2Mouse");
}
public void SetTriggerThreshold(float triggerThreshold)
{
throw new NotImplementedException();

View File

@@ -1,7 +1,6 @@
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Numerics;
@@ -165,8 +164,6 @@ namespace Ryujinx.Input.SDL2
return new SDL2Mouse(this);
}
public IEnumerable<IGamepad> GetGamepads() => [GetGamepad("0")];
public void Dispose()
{
if (_isDisposed)

View File

@@ -1,6 +1,5 @@
using Ryujinx.SDL2.Common;
using System;
using System.Collections.Generic;
namespace Ryujinx.Input.SDL2
{
@@ -52,13 +51,5 @@ namespace Ryujinx.Input.SDL2
return new SDL2Keyboard(this, _keyboardIdentifers[0], "All keyboards");
}
public IEnumerable<IGamepad> GetGamepads()
{
foreach (var keyboardId in _keyboardIdentifers)
{
yield return GetGamepad(keyboardId);
}
}
}
}

View File

@@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
<ProjectReference Include="..\Ryujinx.SDL3.Common\Ryujinx.SDL3.Common.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,388 @@
using Humanizer;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading;
using static SDL3.SDL;
namespace Ryujinx.Input.SDL3
{
class SDL3Gamepad : 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 static readonly Dictionary<GamepadButtonInputId, SDL_GamepadButton> _buttonsDriverDict = new()
{
{ GamepadButtonInputId.LeftStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK },
{ GamepadButtonInputId.DpadUp, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_DPAD_UP },
{ GamepadButtonInputId.DpadDown, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_DPAD_DOWN },
{ GamepadButtonInputId.DpadLeft, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_DPAD_LEFT },
{ GamepadButtonInputId.DpadRight, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_DPAD_RIGHT },
{ GamepadButtonInputId.Minus, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_BACK },
{ GamepadButtonInputId.LeftShoulder, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_SHOULDER },
{ GamepadButtonInputId.LeftTrigger, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE2 },
{ GamepadButtonInputId.RightStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_STICK },
{ GamepadButtonInputId.A, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST },
{ GamepadButtonInputId.B, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH },
{ GamepadButtonInputId.X, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH },
{ GamepadButtonInputId.Y, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST },
{ GamepadButtonInputId.Plus, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START },
{ GamepadButtonInputId.RightShoulder, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER },
{ GamepadButtonInputId.RightTrigger, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2 },
};
private StandardControllerInputConfig _configuration;
private static readonly SDL_GamepadButton[] _buttonsDriverMapping = ToSDLButtonMapping(_buttonsDriverDict);
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 float _triggerThreshold;
public SDL3Gamepad(uint joystickId, string driverId)
{
_gamepadHandle = SDL_OpenGamepad(joystickId);
_buttonsUserMapping = new List<ButtonMappingEntry>(20);
Name = SDL_GetGamepadName(_gamepadHandle);
Id = driverId;
Features = GetFeaturesFlag();
_triggerThreshold = 0.0f;
// Enable motion tracking
if (Features.HasFlag(GamepadFeaturesFlag.Motion))
{
if (!SDL_SetGamepadSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL, true))
{
Logger.Error?.Print(LogClass.Hid,
$"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_ACCEL}.");
}
if (!SDL_SetGamepadSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO, true))
{
Logger.Error?.Print(LogClass.Hid,
$"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_GYRO}.");
}
}
}
private static SDL_GamepadButton[] ToSDLButtonMapping(
Dictionary<GamepadButtonInputId, SDL_GamepadButton> buttonsDriverMapping)
{
return Enumerable.Range(0, (int)GamepadButtonInputId.Count)
.Select(i =>
buttonsDriverMapping.GetValueOrDefault((GamepadButtonInputId)i,
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_INVALID))
.ToArray();
}
private GamepadFeaturesFlag GetFeaturesFlag()
{
GamepadFeaturesFlag result = GamepadFeaturesFlag.None;
if (SDL_GamepadHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL) &&
SDL_GamepadHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO))
{
result |= GamepadFeaturesFlag.Motion;
}
if (SDL_RumbleGamepad(_gamepadHandle, 0, 0, 100))
{
result |= GamepadFeaturesFlag.Rumble;
}
return result;
}
public string Id { get; }
public string Name { get; }
public bool IsConnected => SDL_GamepadConnected(_gamepadHandle);
private void Dispose(bool disposing)
{
if (disposing && _gamepadHandle != nint.Zero)
{
SDL_CloseGamepad(_gamepadHandle);
_gamepadHandle = nint.Zero;
}
}
public void Dispose()
{
Dispose(true);
}
public void SetTriggerThreshold(float triggerThreshold)
{
_triggerThreshold = 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 (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs))
{
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];
if (!SDL_GetGamepadSensorData(_gamepadHandle, sensorType, values, ElementCount))
{
return Vector3.Zero;
}
Vector3 value = new(values[0], values[1], values[2]);
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;
// Then left joycon
_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));
// Finally right joycon
_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));
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);
(short stickX, short stickY) = GetStickXY(inputId);
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 (resultX, resultY);
}
// ReSharper disable once InconsistentNaming
private (short, short) GetStickXY(StickInputId inputId) =>
inputId switch
{
StickInputId.Left => (
SDL_GetGamepadAxis(_gamepadHandle, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_LEFTX),
SDL_GetGamepadAxis(_gamepadHandle, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_LEFTY)),
StickInputId.Right => (
SDL_GetGamepadAxis(_gamepadHandle, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_RIGHTX),
SDL_GetGamepadAxis(_gamepadHandle, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_RIGHTY)),
_ => throw new NotSupportedException($"Unsupported stick {inputId}")
};
public bool IsPressed(GamepadButtonInputId inputId)
{
switch (inputId)
{
case GamepadButtonInputId.LeftTrigger:
return ConvertRawStickValue(SDL_GetGamepadAxis(_gamepadHandle,
SDL_GamepadAxis.SDL_GAMEPAD_AXIS_LEFT_TRIGGER)) > _triggerThreshold;
case GamepadButtonInputId.RightTrigger:
return ConvertRawStickValue(SDL_GetGamepadAxis(_gamepadHandle,
SDL_GamepadAxis.SDL_GAMEPAD_AXIS_RIGHT_TRIGGER)) > _triggerThreshold;
}
var button = _buttonsDriverMapping[(int)inputId];
if (button == SDL_GamepadButton.SDL_GAMEPAD_BUTTON_INVALID)
{
return false;
}
return SDL_GetGamepadButton(_gamepadHandle, button);
}
}
}

View File

@@ -0,0 +1,192 @@
using Ryujinx.Common.Logging;
using Ryujinx.Input.SDL3;
using Ryujinx.SDL3.Common;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using static SDL3.SDL;
namespace Ryujinx.Input.SDL3
{
public class SDL3GamepadDriver : IGamepadDriver
{
private readonly Dictionary<uint, string> _gamepadsInstanceIdsMapping;
private readonly List<string> _gamepadsIds;
private readonly Lock _lock = new();
public ReadOnlySpan<string> GamepadsIds
{
get
{
lock (_lock)
{
return _gamepadsIds.ToArray();
}
}
}
public string DriverName => "SDL3";
public event Action<string> OnGamepadConnected;
public event Action<string> OnGamepadDisconnected;
public SDL3GamepadDriver()
{
_gamepadsInstanceIdsMapping = new Dictionary<uint, string>();
_gamepadsIds = new List<string>();
SDL3Driver.Instance.Initialize();
SDL3Driver.Instance.OnJoyStickConnected += HandleJoyStickConnected;
SDL3Driver.Instance.OnJoystickDisconnected += HandleJoyStickDisconnected;
SDL3Driver.Instance.OnJoyBatteryUpdated += HandleJoyBatteryUpdated;
}
private string GenerateGamepadId(uint joystickId)
{
int bufferSize = 33;
Span<byte> pszGuid = stackalloc byte[bufferSize];
SDL_GUIDToString(SDL_GetJoystickGUIDForID(joystickId), pszGuid, bufferSize);
var guid = Encoding.UTF8.GetString(pszGuid);
string id;
lock (_lock)
{
int guidIndex = 0;
id = guidIndex + guid;
while (_gamepadsIds.Contains(id))
{
id = (++guidIndex) + "-" + guid;
}
}
return id;
}
private KeyValuePair<uint,string> GetGamepadInfoByGamepadId(string id)
{
lock (_lock)
{
return _gamepadsInstanceIdsMapping.FirstOrDefault(gamepadId => gamepadId.Value == id);
}
}
private void HandleJoyStickDisconnected(uint joystickId)
{
bool joyConPairDisconnected = false;
if (!_gamepadsInstanceIdsMapping.Remove(joystickId, out string id))
return;
lock (_lock)
{
_gamepadsIds.Remove(id);
if (!SDL3JoyConPair.IsCombinable(_gamepadsInstanceIdsMapping))
{
_gamepadsIds.Remove(SDL3JoyConPair.Id);
joyConPairDisconnected = true;
}
}
OnGamepadDisconnected?.Invoke(id);
if (joyConPairDisconnected)
{
OnGamepadDisconnected?.Invoke(SDL3JoyConPair.Id);
}
}
private void HandleJoyStickConnected(uint joystickId)
{
bool joyConPairConnected = false;
if (_gamepadsInstanceIdsMapping.ContainsKey(joystickId))
{
// Sometimes a JoyStick connected event fires after the app starts even though it was connected before
// so it is rejected to avoid doubling the entries.
return;
}
string id = GenerateGamepadId(joystickId);
if (id == null)
{
return;
}
if (_gamepadsInstanceIdsMapping.TryAdd(joystickId, id))
{
lock (_lock)
{
_gamepadsIds.Add(id);
if (SDL3JoyConPair.IsCombinable(_gamepadsInstanceIdsMapping))
{
_gamepadsIds.Remove(SDL3JoyConPair.Id);
_gamepadsIds.Add(SDL3JoyConPair.Id);
joyConPairConnected = true;
}
}
OnGamepadConnected?.Invoke(id);
if (joyConPairConnected)
{
OnGamepadConnected?.Invoke(SDL3JoyConPair.Id);
}
}
}
private void HandleJoyBatteryUpdated(uint joystickId, SDL_JoyBatteryEvent joyBatteryEvent)
{
Logger.Info?.Print(LogClass.Hid,
$"{SDL_GetGamepadNameForID(joystickId)}, Battery percent: {joyBatteryEvent.percent}");
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
SDL3Driver.Instance.OnJoyStickConnected -= HandleJoyStickConnected;
SDL3Driver.Instance.OnJoystickDisconnected -= HandleJoyStickDisconnected;
// Simulate a full disconnect when disposing
foreach (string id in _gamepadsIds)
{
OnGamepadDisconnected?.Invoke(id);
}
lock (_lock)
{
_gamepadsIds.Clear();
}
SDL3Driver.Instance.Dispose();
}
}
public void Dispose()
{
GC.SuppressFinalize(this);
Dispose(true);
}
public IGamepad GetGamepad(string id)
{
if (id == SDL3JoyConPair.Id)
{
lock (_lock)
{
return SDL3JoyConPair.GetGamepad(_gamepadsInstanceIdsMapping);
}
}
var gamepadInfo = GetGamepadInfoByGamepadId(id);
if (SDL3JoyCon.IsJoyCon(gamepadInfo.Key))
{
return new SDL3JoyCon(gamepadInfo.Key, gamepadInfo.Value);
}
return new SDL3Gamepad(gamepadInfo.Key, gamepadInfo.Value);
}
}
}

View File

@@ -0,0 +1,418 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading;
using static SDL3.SDL;
namespace Ryujinx.Input.SDL3
{
class SDL3JoyCon : 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 static readonly Dictionary<GamepadButtonInputId, SDL_GamepadButton> _leftButtonsDriverDict = new()
{
{ GamepadButtonInputId.LeftStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK },
{ GamepadButtonInputId.DpadUp, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST },
{ GamepadButtonInputId.DpadDown, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST },
{ GamepadButtonInputId.DpadLeft, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH },
{ GamepadButtonInputId.DpadRight, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH },
{ GamepadButtonInputId.Minus, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START },
{ GamepadButtonInputId.LeftShoulder, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE1 },
{ GamepadButtonInputId.LeftTrigger, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE2 },
{ GamepadButtonInputId.SingleRightTrigger0, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER },
{ GamepadButtonInputId.SingleLeftTrigger0, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_SHOULDER },
};
private static readonly Dictionary<GamepadButtonInputId, SDL_GamepadButton> _rightButtonsDriverDict = new()
{
{ GamepadButtonInputId.RightStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK },
{ GamepadButtonInputId.A, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH },
{ GamepadButtonInputId.B, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST },
{ GamepadButtonInputId.X, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST },
{ GamepadButtonInputId.Y, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH },
{ GamepadButtonInputId.Plus, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START },
{ GamepadButtonInputId.RightShoulder, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1 },
{ GamepadButtonInputId.RightTrigger, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2 },
{ GamepadButtonInputId.SingleRightTrigger1, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER },
{ GamepadButtonInputId.SingleLeftTrigger1, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_SHOULDER }
};
private readonly SDL_GamepadButton[] _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 readonly SDL_GamepadType _gamepadType;
public SDL3JoyCon(uint joystickId, string driverId)
{
_gamepadHandle = SDL_OpenGamepad(joystickId);
_buttonsUserMapping = new List<ButtonMappingEntry>(10);
Name = SDL_GetGamepadName(_gamepadHandle);
Id = driverId;
Features = GetFeaturesFlag();
// Enable motion tracking
if (Features.HasFlag(GamepadFeaturesFlag.Motion))
{
if (!SDL_SetGamepadSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL, true))
{
Logger.Error?.Print(LogClass.Hid,
$"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_ACCEL}.");
}
if (!SDL_SetGamepadSensorEnabled(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO, true))
{
Logger.Error?.Print(LogClass.Hid,
$"Could not enable data reporting for SensorType {SDL_SensorType.SDL_SENSOR_GYRO}.");
}
}
_gamepadType = SDL_GetGamepadType(_gamepadHandle);
_buttonsDriverMapping = _gamepadType switch
{
SDL_GamepadType.SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT => ToSDLButtonMapping(
_leftButtonsDriverDict),
SDL_GamepadType.SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT => ToSDLButtonMapping(
_rightButtonsDriverDict),
_ => throw new InvalidOperationException($"Unexpected JoyConType value: {_gamepadType}")
};
}
private static SDL_GamepadButton[] ToSDLButtonMapping(
Dictionary<GamepadButtonInputId, SDL_GamepadButton> buttonsDriverDict)
{
return Enumerable.Range(0, (int)GamepadButtonInputId.Count)
.Select(i =>
buttonsDriverDict.GetValueOrDefault((GamepadButtonInputId)i,
SDL_GamepadButton.SDL_GAMEPAD_BUTTON_INVALID))
.ToArray();
}
private GamepadFeaturesFlag GetFeaturesFlag()
{
GamepadFeaturesFlag result = GamepadFeaturesFlag.None;
if (SDL_GamepadHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_ACCEL) &&
SDL_GamepadHasSensor(_gamepadHandle, SDL_SensorType.SDL_SENSOR_GYRO))
{
result |= GamepadFeaturesFlag.Motion;
}
if (SDL_RumbleGamepad(_gamepadHandle, 0, 0, 100))
{
result |= GamepadFeaturesFlag.Rumble;
}
return result;
}
public string Id { get; }
public string Name { get; }
public bool IsConnected => SDL_GamepadConnected(_gamepadHandle);
private void Dispose(bool disposing)
{
if (disposing && _gamepadHandle != nint.Zero)
{
SDL_CloseGamepad(_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 (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs))
{
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];
if (!SDL_GetGamepadSensorData(_gamepadHandle, sensorType, values, ElementCount))
{
return Vector3.Zero;
}
Vector3 value = _gamepadType switch
{
SDL_GamepadType.SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT => new Vector3(-values[2], values[1], values[0]),
SDL_GamepadType.SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT => new Vector3(values[2], values[1], -values[0]),
_ => throw new ArgumentOutOfRangeException($"Unexpected JoyConType value: {_gamepadType}")
};
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 (_gamepadType)
{
case SDL_GamepadType.SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_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 SDL_GamepadType.SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_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 && _gamepadType == SDL_GamepadType.SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT ||
inputId == StickInputId.Right && _gamepadType == SDL_GamepadType.SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_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 _gamepadType == SDL_GamepadType.SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT => (resultY, -resultX),
StickInputId.Right when _gamepadType == SDL_GamepadType.SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT => (-resultY, resultX),
_ => (0.0f, 0.0f)
};
}
private (short, short) GetStickXY()
{
return (
SDL_GetGamepadAxis(_gamepadHandle, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_LEFTX),
SDL_GetGamepadAxis(_gamepadHandle, SDL_GamepadAxis.SDL_GAMEPAD_AXIS_LEFTY));
}
public bool IsPressed(GamepadButtonInputId inputId)
{
var button = _buttonsDriverMapping[(int)inputId];
if (button == SDL_GamepadButton.SDL_GAMEPAD_BUTTON_INVALID)
{
return false;
}
return SDL_GetGamepadButton(_gamepadHandle, button);
}
public static bool IsJoyCon(uint joystickId)
{
var gamepadName = SDL_GetGamepadTypeForID(joystickId);
return gamepadName is SDL_GamepadType.SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT
or SDL_GamepadType.SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT;
}
}
}

View File

@@ -0,0 +1,215 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading;
using static SDL3.SDL;
namespace Ryujinx.Input.SDL3
{
class SDL3JoyConPair(SDL3JoyCon left, SDL3JoyCon right) : IGamepad
{
private StandardControllerInputConfig _configuration;
private readonly record struct ButtonMappingEntry(GamepadButtonInputId To, GamepadButtonInputId From)
{
public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not GamepadButtonInputId.Unbound;
}
private readonly StickInputId[] _stickUserMapping = new StickInputId[(int)StickInputId.Count]
{
StickInputId.Unbound, StickInputId.Left, StickInputId.Right,
};
public GamepadFeaturesFlag Features => (left?.Features ?? GamepadFeaturesFlag.None) |
(right?.Features ?? GamepadFeaturesFlag.None);
public const string Id = "JoyConPair";
private readonly Lock _userMappingLock = new();
private readonly List<ButtonMappingEntry> _buttonsUserMapping = new List<ButtonMappingEntry>(20);
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()
{
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;
}
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)
{
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;
// Then left joycon
_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));
// Finally right joycon
_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));
left.SetConfiguration(configuration);
right.SetConfiguration(configuration);
}
}
public void SetTriggerThreshold(float triggerThreshold)
{
}
public static bool IsCombinable(Dictionary<uint, string> gamepadsInstanceIdsMapping)
{
var gamepadTypes = gamepadsInstanceIdsMapping.Keys.Select(SDL_GetGamepadTypeForID).ToArray();
return gamepadTypes.Contains(SDL_GamepadType.SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT) &&
gamepadTypes.Contains(SDL_GamepadType.SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT);
}
public static IGamepad GetGamepad(Dictionary<uint, string> gamepadsInstanceIdsMapping)
{
var leftPair =
gamepadsInstanceIdsMapping.FirstOrDefault(pair =>
SDL_GetGamepadTypeForID(pair.Key) == SDL_GamepadType.SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_LEFT);
var rightPair =
gamepadsInstanceIdsMapping.FirstOrDefault(pair =>
SDL_GetGamepadTypeForID(pair.Key) == SDL_GamepadType.SDL_GAMEPAD_TYPE_NINTENDO_SWITCH_JOYCON_RIGHT);
if (leftPair.Key == 0 || rightPair.Key == 0)
{
return null;
}
return new SDL3JoyConPair(new SDL3JoyCon(leftPair.Key, leftPair.Value),
new SDL3JoyCon(rightPair.Key, rightPair.Value));
}
}
}

View File

@@ -0,0 +1,405 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading;
using static SDL3.SDL;
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
namespace Ryujinx.Input.SDL3
{
class SDL3Keyboard : IKeyboard
{
private readonly record struct ButtonMappingEntry(GamepadButtonInputId To, Key From)
{
public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not Key.Unbound;
}
private readonly Lock _userMappingLock = new();
#pragma warning disable IDE0052 // Remove unread private member
private readonly SDL3KeyboardDriver _driver;
#pragma warning restore IDE0052
private StandardKeyboardInputConfig _configuration;
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
private static readonly SDL_Keycode[] _keysDriverMapping = new SDL_Keycode[(int)Key.Count]
{
// INVALID
SDL_Keycode.SDLK_0,
// Presented as modifiers, so invalid here.
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_F1,
SDL_Keycode.SDLK_F2,
SDL_Keycode.SDLK_F3,
SDL_Keycode.SDLK_F4,
SDL_Keycode.SDLK_F5,
SDL_Keycode.SDLK_F6,
SDL_Keycode.SDLK_F7,
SDL_Keycode.SDLK_F8,
SDL_Keycode.SDLK_F9,
SDL_Keycode.SDLK_F10,
SDL_Keycode.SDLK_F11,
SDL_Keycode.SDLK_F12,
SDL_Keycode.SDLK_F13,
SDL_Keycode.SDLK_F14,
SDL_Keycode.SDLK_F15,
SDL_Keycode.SDLK_F16,
SDL_Keycode.SDLK_F17,
SDL_Keycode.SDLK_F18,
SDL_Keycode.SDLK_F19,
SDL_Keycode.SDLK_F20,
SDL_Keycode.SDLK_F21,
SDL_Keycode.SDLK_F22,
SDL_Keycode.SDLK_F23,
SDL_Keycode.SDLK_F24,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_UP,
SDL_Keycode.SDLK_DOWN,
SDL_Keycode.SDLK_LEFT,
SDL_Keycode.SDLK_RIGHT,
SDL_Keycode.SDLK_RETURN,
SDL_Keycode.SDLK_ESCAPE,
SDL_Keycode.SDLK_SPACE,
SDL_Keycode.SDLK_TAB,
SDL_Keycode.SDLK_BACKSPACE,
SDL_Keycode.SDLK_INSERT,
SDL_Keycode.SDLK_DELETE,
SDL_Keycode.SDLK_PAGEUP,
SDL_Keycode.SDLK_PAGEDOWN,
SDL_Keycode.SDLK_HOME,
SDL_Keycode.SDLK_END,
SDL_Keycode.SDLK_CAPSLOCK,
SDL_Keycode.SDLK_SCROLLLOCK,
SDL_Keycode.SDLK_PRINTSCREEN,
SDL_Keycode.SDLK_PAUSE,
SDL_Keycode.SDLK_NUMLOCKCLEAR,
SDL_Keycode.SDLK_CLEAR,
SDL_Keycode.SDLK_KP_0,
SDL_Keycode.SDLK_KP_1,
SDL_Keycode.SDLK_KP_2,
SDL_Keycode.SDLK_KP_3,
SDL_Keycode.SDLK_KP_4,
SDL_Keycode.SDLK_KP_5,
SDL_Keycode.SDLK_KP_6,
SDL_Keycode.SDLK_KP_7,
SDL_Keycode.SDLK_KP_8,
SDL_Keycode.SDLK_KP_9,
SDL_Keycode.SDLK_KP_DIVIDE,
SDL_Keycode.SDLK_KP_MULTIPLY,
SDL_Keycode.SDLK_KP_MINUS,
SDL_Keycode.SDLK_KP_PLUS,
SDL_Keycode.SDLK_KP_DECIMAL,
SDL_Keycode.SDLK_KP_ENTER,
SDL_Keycode.SDLK_A,
SDL_Keycode.SDLK_B,
SDL_Keycode.SDLK_C,
SDL_Keycode.SDLK_D,
SDL_Keycode.SDLK_E,
SDL_Keycode.SDLK_F,
SDL_Keycode.SDLK_G,
SDL_Keycode.SDLK_H,
SDL_Keycode.SDLK_I,
SDL_Keycode.SDLK_J,
SDL_Keycode.SDLK_K,
SDL_Keycode.SDLK_L,
SDL_Keycode.SDLK_M,
SDL_Keycode.SDLK_N,
SDL_Keycode.SDLK_O,
SDL_Keycode.SDLK_P,
SDL_Keycode.SDLK_Q,
SDL_Keycode.SDLK_R,
SDL_Keycode.SDLK_S,
SDL_Keycode.SDLK_T,
SDL_Keycode.SDLK_U,
SDL_Keycode.SDLK_V,
SDL_Keycode.SDLK_W,
SDL_Keycode.SDLK_X,
SDL_Keycode.SDLK_Y,
SDL_Keycode.SDLK_Z,
SDL_Keycode.SDLK_0,
SDL_Keycode.SDLK_1,
SDL_Keycode.SDLK_2,
SDL_Keycode.SDLK_3,
SDL_Keycode.SDLK_4,
SDL_Keycode.SDLK_5,
SDL_Keycode.SDLK_6,
SDL_Keycode.SDLK_7,
SDL_Keycode.SDLK_8,
SDL_Keycode.SDLK_9,
SDL_Keycode.SDLK_GRAVE,
SDL_Keycode.SDLK_GRAVE,
SDL_Keycode.SDLK_MINUS,
SDL_Keycode.SDLK_PLUS,
SDL_Keycode.SDLK_LEFTBRACKET,
SDL_Keycode.SDLK_RIGHTBRACKET,
SDL_Keycode.SDLK_SEMICOLON,
SDL_Keycode.SDLK_APOSTROPHE,
SDL_Keycode.SDLK_COMMA,
SDL_Keycode.SDLK_PERIOD,
SDL_Keycode.SDLK_SLASH,
SDL_Keycode.SDLK_BACKSLASH,
// Invalids
SDL_Keycode.SDLK_0,
};
public SDL3Keyboard(SDL3KeyboardDriver driver, string id, string name)
{
_driver = driver;
Id = id;
Name = name;
_buttonsUserMapping = new List<ButtonMappingEntry>();
}
private bool HasConfiguration => _configuration != null;
public string Id { get; }
public string Name { get; }
public bool IsConnected => true;
public GamepadFeaturesFlag Features => GamepadFeaturesFlag.None;
public void Dispose()
{
// No operations
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static int ToSDL3Scancode(Key key)
{
if (key >= Key.Unknown && key <= Key.Menu)
{
return -1;
}
IntPtr modstate = (int)SDL_Keymod.SDL_KMOD_NONE;
return (int)SDL_GetScancodeFromKey((uint)_keysDriverMapping[(int)key], modstate);
}
private static SDL_Keymod GetKeyboardModifierMask(Key key)
{
return key switch
{
Key.ShiftLeft => SDL_Keymod.SDL_KMOD_LSHIFT,
Key.ShiftRight => SDL_Keymod.SDL_KMOD_RSHIFT,
Key.ControlLeft => SDL_Keymod.SDL_KMOD_LCTRL,
Key.ControlRight => SDL_Keymod.SDL_KMOD_RCTRL,
Key.AltLeft => SDL_Keymod.SDL_KMOD_LALT,
Key.AltRight => SDL_Keymod.SDL_KMOD_RALT,
Key.WinLeft => SDL_Keymod.SDL_KMOD_LGUI,
Key.WinRight => SDL_Keymod.SDL_KMOD_RGUI,
// NOTE: Menu key isn't supported by SDL3.
_ => SDL_Keymod.SDL_KMOD_NONE,
};
}
public KeyboardStateSnapshot GetKeyboardStateSnapshot()
{
Span<SDLBool> rawKeyboardState;
SDL_Keymod rawKeyboardModifierState = SDL_GetModState();
unsafe
{
rawKeyboardState = SDL_GetKeyboardState(out int numKeys);
}
bool[] keysState = new bool[(int)Key.Count];
for (Key key = 0; key < Key.Count; key++)
{
int index = ToSDL3Scancode(key);
if (index == -1)
{
SDL_Keymod modifierMask = GetKeyboardModifierMask(key);
if (modifierMask == SDL_Keymod.SDL_KMOD_NONE)
{
continue;
}
keysState[(int)key] = (rawKeyboardModifierState & modifierMask) == modifierMask;
}
else
{
keysState[(int)key] = rawKeyboardState[index];
}
}
return new KeyboardStateSnapshot(keysState);
}
private static float ConvertRawStickValue(short value)
{
const float ConvertRate = 1.0f / (short.MaxValue + 0.5f);
return value * ConvertRate;
}
private static (short, short) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick<ConfigKey> stickConfig)
{
short stickX = 0;
short stickY = 0;
if (snapshot.IsPressed((Key)stickConfig.StickUp))
{
stickY += 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickDown))
{
stickY -= 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickRight))
{
stickX += 1;
}
if (snapshot.IsPressed((Key)stickConfig.StickLeft))
{
stickX -= 1;
}
Vector2 stick = Vector2.Normalize(new Vector2(stickX, stickY));
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
}
public GamepadStateSnapshot GetMappedStateSnapshot()
{
KeyboardStateSnapshot rawState = GetKeyboardStateSnapshot();
GamepadStateSnapshot result = default;
lock (_userMappingLock)
{
if (!HasConfiguration)
{
return result;
}
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
{
if (entry.From == Key.Unknown || entry.From == Key.Unbound || entry.To == GamepadButtonInputId.Unbound)
{
continue;
}
// Do not touch state of button already pressed
if (!result.IsPressed(entry.To))
{
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
}
}
(short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick);
(short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick);
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
}
return result;
}
public GamepadStateSnapshot GetStateSnapshot()
{
throw new NotSupportedException();
}
public (float, float) GetStick(StickInputId inputId)
{
throw new NotSupportedException();
}
public bool IsPressed(GamepadButtonInputId inputId)
{
throw new NotSupportedException();
}
public bool IsPressed(Key key)
{
// We only implement GetKeyboardStateSnapshot.
throw new NotSupportedException();
}
public void SetConfiguration(InputConfig configuration)
{
lock (_userMappingLock)
{
_configuration = (StandardKeyboardInputConfig)configuration;
// First clear the buttons mapping
_buttonsUserMapping.Clear();
// Then configure left joycon
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftStick, (Key)_configuration.LeftJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadUp, (Key)_configuration.LeftJoycon.DpadUp));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadDown, (Key)_configuration.LeftJoycon.DpadDown));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadLeft, (Key)_configuration.LeftJoycon.DpadLeft));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.DpadRight, (Key)_configuration.LeftJoycon.DpadRight));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Minus, (Key)_configuration.LeftJoycon.ButtonMinus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftShoulder, (Key)_configuration.LeftJoycon.ButtonL));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.LeftTrigger, (Key)_configuration.LeftJoycon.ButtonZl));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger0, (Key)_configuration.LeftJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger0, (Key)_configuration.LeftJoycon.ButtonSl));
// Finally configure right joycon
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightStick, (Key)_configuration.RightJoyconStick.StickButton));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.A, (Key)_configuration.RightJoycon.ButtonA));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.B, (Key)_configuration.RightJoycon.ButtonB));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.X, (Key)_configuration.RightJoycon.ButtonX));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Y, (Key)_configuration.RightJoycon.ButtonY));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.Plus, (Key)_configuration.RightJoycon.ButtonPlus));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightShoulder, (Key)_configuration.RightJoycon.ButtonR));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.RightTrigger, (Key)_configuration.RightJoycon.ButtonZr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleRightTrigger1, (Key)_configuration.RightJoycon.ButtonSr));
_buttonsUserMapping.Add(new ButtonMappingEntry(GamepadButtonInputId.SingleLeftTrigger1, (Key)_configuration.RightJoycon.ButtonSl));
}
}
public void SetTriggerThreshold(float triggerThreshold)
{
// No operations
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
// No operations
}
public Vector3 GetMotionData(MotionInputId inputId)
{
// No operations
return Vector3.Zero;
}
}
}

View File

@@ -0,0 +1,55 @@
using Ryujinx.SDL3.Common;
using System;
namespace Ryujinx.Input.SDL3
{
public class SDL3KeyboardDriver : IGamepadDriver
{
public SDL3KeyboardDriver()
{
SDL3Driver.Instance.Initialize();
}
public string DriverName => "SDL3";
private static readonly string[] _keyboardIdentifers = new string[1] { "0" };
public ReadOnlySpan<string> GamepadsIds => _keyboardIdentifers;
public event Action<string> OnGamepadConnected
{
add { }
remove { }
}
public event Action<string> OnGamepadDisconnected
{
add { }
remove { }
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
SDL3Driver.Instance.Dispose();
}
}
public void Dispose()
{
GC.SuppressFinalize(this);
Dispose(true);
}
public IGamepad GetGamepad(string id)
{
if (!_keyboardIdentifers[0].Equals(id))
{
return null;
}
return new SDL3Keyboard(this, _keyboardIdentifers[0], "All keyboards");
}
}
}

View File

@@ -0,0 +1,90 @@
using Ryujinx.Common.Configuration.Hid;
using System;
using System.Drawing;
using System.Numerics;
namespace Ryujinx.Input.SDL3
{
public class SDL3Mouse : IMouse
{
private SDL3MouseDriver _driver;
public GamepadFeaturesFlag Features => throw new NotImplementedException();
public string Id => "0";
public string Name => "SDL3Mouse";
public bool IsConnected => true;
public bool[] Buttons => _driver.PressedButtons;
Size IMouse.ClientSize => _driver.GetClientSize();
public SDL3Mouse(SDL3MouseDriver driver)
{
_driver = driver;
}
public Vector2 GetPosition()
{
return _driver.CurrentPosition;
}
public Vector2 GetScroll()
{
return _driver.Scroll;
}
public GamepadStateSnapshot GetMappedStateSnapshot()
{
throw new NotImplementedException();
}
public Vector3 GetMotionData(MotionInputId inputId)
{
throw new NotImplementedException();
}
public GamepadStateSnapshot GetStateSnapshot()
{
throw new NotImplementedException();
}
public (float, float) GetStick(StickInputId inputId)
{
throw new NotImplementedException();
}
public bool IsButtonPressed(MouseButton button)
{
return _driver.IsButtonPressed(button);
}
public bool IsPressed(GamepadButtonInputId inputId)
{
throw new NotImplementedException();
}
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
{
throw new NotImplementedException();
}
public void SetConfiguration(InputConfig configuration)
{
throw new NotImplementedException();
}
public void SetTriggerThreshold(float triggerThreshold)
{
throw new NotImplementedException();
}
public void Dispose()
{
GC.SuppressFinalize(this);
_driver = null;
}
}
}

View File

@@ -0,0 +1,179 @@
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using System;
using System.Diagnostics;
using System.Drawing;
using System.Numerics;
using System.Runtime.CompilerServices;
using static SDL3.SDL;
namespace Ryujinx.Input.SDL3
{
public class SDL3MouseDriver : IGamepadDriver
{
private const int CursorHideIdleTime = 5; // seconds
private bool _isDisposed;
private readonly HideCursorMode _hideCursorMode;
private bool _isHidden;
private long _lastCursorMoveTime;
public bool[] PressedButtons { get; }
public Vector2 CurrentPosition { get; private set; }
public Vector2 Scroll { get; private set; }
public Size ClientSize;
public SDL3MouseDriver(HideCursorMode hideCursorMode)
{
PressedButtons = new bool[(int)MouseButton.Count];
_hideCursorMode = hideCursorMode;
if (_hideCursorMode == HideCursorMode.Always)
{
if (!SDL_HideCursor())
{
Logger.Error?.PrintMsg(LogClass.Application, "Failed to disable the cursor.");
}
_isHidden = true;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static MouseButton DriverButtonToMouseButton(uint rawButton)
{
Debug.Assert(rawButton is > 0 and <= (int)MouseButton.Count);
return (MouseButton)(rawButton - 1);
}
public void UpdatePosition()
{
_ = SDL_GetMouseState(out float posX, out float posY);
Vector2 position = new(posX, posY);
if (CurrentPosition != position)
{
CurrentPosition = position;
_lastCursorMoveTime = Stopwatch.GetTimestamp();
}
CheckIdle();
}
private void CheckIdle()
{
if (_hideCursorMode != HideCursorMode.OnIdle)
{
return;
}
long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
if (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency)
{
if (!_isHidden)
{
if (!SDL_HideCursor())
{
Logger.Error?.PrintMsg(LogClass.Application, "Failed to disable the cursor.");
}
_isHidden = true;
}
}
else
{
if (_isHidden)
{
if (!SDL_HideCursor())
{
Logger.Error?.PrintMsg(LogClass.Application, "Failed to enable the cursor.");
}
_isHidden = false;
}
}
}
public void Update(SDL_Event evnt)
{
var type = (SDL_EventType)evnt.type;
switch (type)
{
case SDL_EventType.SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EventType.SDL_EVENT_MOUSE_BUTTON_UP:
uint rawButton = evnt.button.button;
if (rawButton > 0 && rawButton <= (int)MouseButton.Count)
{
PressedButtons[(int)DriverButtonToMouseButton(rawButton)] = type == SDL_EventType.SDL_EVENT_MOUSE_BUTTON_DOWN;
CurrentPosition = new Vector2(evnt.button.x, evnt.button.y);
}
break;
// NOTE: On Linux using Wayland mouse motion events won't be received at all.
case SDL_EventType.SDL_EVENT_MOUSE_MOTION:
CurrentPosition = new Vector2(evnt.motion.x, evnt.motion.y);
_lastCursorMoveTime = Stopwatch.GetTimestamp();
break;
case SDL_EventType.SDL_EVENT_MOUSE_WHEEL:
Scroll = new Vector2(evnt.wheel.x, evnt.wheel.y);
break;
}
}
public void SetClientSize(int width, int height)
{
ClientSize = new Size(width, height);
}
public bool IsButtonPressed(MouseButton button)
{
return PressedButtons[(int)button];
}
public Size GetClientSize()
{
return ClientSize;
}
public string DriverName => "SDL3";
public event Action<string> OnGamepadConnected
{
add { }
remove { }
}
public event Action<string> OnGamepadDisconnected
{
add { }
remove { }
}
public ReadOnlySpan<string> GamepadsIds => new[] { "0" };
public IGamepad GetGamepad(string id)
{
return new SDL3Mouse(this);
}
public void Dispose()
{
if (_isDisposed)
{
return;
}
GC.SuppressFinalize(this);
_isDisposed = true;
}
}
}

View File

@@ -24,10 +24,5 @@ namespace Ryujinx.Input
/// <remarks>Also named sixaxis</remarks>
/// </summary>
Motion,
/// <summary>
/// The LED on the back of modern PlayStation controllers (DualSense &amp; DualShock 4).
/// </summary>
Led,
}
}

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;
}
}
@@ -377,6 +392,11 @@ namespace Ryujinx.Input.HLE
return state;
}
public static JoystickPosition GetJoystickPosition(float x, float y, float deadzone, float range)
{
return ClampToCircle(ApplyDeadzone(x, y,deadzone), range);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static JoystickPosition ApplyDeadzone(float x, float y, float deadzone)

View File

@@ -65,15 +65,6 @@ namespace Ryujinx.Input
/// <param name="configuration">The configuration of the gamepad</param>
void SetConfiguration(InputConfig configuration);
/// <summary>
/// Set the LED on the gamepad to a given color.
/// </summary>
/// <remarks>Does nothing on a controller without LED functionality.</remarks>
/// <param name="packedRgb">The packed RGB integer.</param>
void SetLed(uint packedRgb);
public void ClearLed() => SetLed(0);
/// <summary>
/// Starts a rumble effect on the gamepad.
/// </summary>

View File

@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
namespace Ryujinx.Input
{
@@ -34,11 +33,6 @@ namespace Ryujinx.Input
/// <param name="id">The unique id of the gamepad</param>
/// <returns>An instance of <see cref="IGamepad"/> associated to the gamepad id given or null if not found</returns>
IGamepad GetGamepad(string id);
/// <summary>
/// Returns an <see cref="IEnumerable{T}"/> of the connected gamepads.
/// </summary>
IEnumerable<IGamepad> GetGamepads();
/// <summary>
/// Clear the internal state of the driver.

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

@@ -1,6 +1,5 @@
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Common.Utilities;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -26,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;
@@ -79,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.
@@ -144,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))
{
@@ -168,8 +177,6 @@ namespace Ryujinx.SDL2.Common
HandleSDLEvent(ref evnt);
}
});
Rainbow.Tick();
waitHandle.Wait(WaitTimeMs);
}

View File

@@ -0,0 +1,31 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>Ryujinx.SDL3_CS</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup Condition="'$(OS)' == 'Windows_NT'">
<None Include="runtimes\win-x64\native\SDL3.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Link>SDL3.dll</Link>
</None>
</ItemGroup>
<!-- <ItemGroup Condition="'$(OS)' == 'Linux'">-->
<!-- <None Include="runtimes\linux-x64\native\SDL3.dll">-->
<!-- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>-->
<!-- <DestinationFolder>$(ProjectDir)</DestinationFolder>-->
<!-- </None>-->
<!-- </ItemGroup>-->
<!-- <ItemGroup Condition="'$(OS)' == 'Darwin'">-->
<!-- <None Include="runtimes\osx-x64\native\SDL3.dll">-->
<!-- <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>-->
<!-- <DestinationFolder>$(ProjectDir)</DestinationFolder>-->
<!-- </None>-->
<!-- </ItemGroup>-->
</Project>

8079
src/Ryujinx.SDL3-CS/SDL3.cs Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<DefaultItemExcludes>$(DefaultItemExcludes);._*</DefaultItemExcludes>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
<ProjectReference Include="..\Ryujinx.SDL3-CS\Ryujinx.SDL3-CS.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,210 @@
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using static SDL3.SDL;
namespace Ryujinx.SDL3.Common
{
public class SDL3Driver : IDisposable
{
private static SDL3Driver _instance;
public static SDL3Driver Instance
{
get
{
_instance ??= new SDL3Driver();
return _instance;
}
}
public static Action<Action> MainThreadDispatcher { get; set; }
private const SDL_InitFlags SdlInitFlags = SDL_InitFlags.SDL_INIT_GAMEPAD | SDL_InitFlags.SDL_INIT_AUDIO |
SDL_InitFlags.SDL_INIT_VIDEO;
private bool _isRunning;
private uint _refereceCount;
private Thread _worker;
public event Action<uint> OnJoyStickConnected;
public event Action<uint> OnJoystickDisconnected;
public event Action<uint, SDL_JoyBatteryEvent> OnJoyBatteryUpdated;
private ConcurrentDictionary<uint, Action<SDL_Event>> _registeredWindowHandlers;
private readonly Lock _lock = new();
private SDL3Driver() { }
public void Initialize()
{
lock (_lock)
{
_refereceCount++;
if (_isRunning)
{
return;
}
SDL_SetHint(SDL_HINT_APP_NAME, "Ryujinx");
// SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS4_RUMBLE, "1");
// SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_SWITCH_HOME_LED, "0");
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
//
//
// // NOTE: As of SDL2 2.24.0, joycons are combined by default but the motion source only come from one of them.
// // We disable this behavior for now.
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_COMBINE_JOY_CONS, "0");
if (!SDL_Init(SdlInitFlags))
{
string errorMessage = $"SDL3 initialization failed with error \"{SDL_GetError()}\"";
Logger.Error?.Print(LogClass.Application, errorMessage);
throw new Exception(errorMessage);
}
// First ensure that we only enable joystick events (for connected/disconnected).
if (!SDL_GamepadEventsEnabled())
{
Logger.Error?.PrintMsg(LogClass.Application,
"Couldn't change the state of game controller events.");
}
if (!SDL_JoystickEventsEnabled())
{
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.
SDL_SetEventEnabled((uint)SDL_EventType.SDL_EVENT_JOYSTICK_AXIS_MOTION, false);
SDL_SetEventEnabled((uint)SDL_EventType.SDL_EVENT_JOYSTICK_BALL_MOTION, false);
SDL_SetEventEnabled((uint)SDL_EventType.SDL_EVENT_JOYSTICK_HAT_MOTION, false);
SDL_SetEventEnabled((uint)SDL_EventType.SDL_EVENT_JOYSTICK_BUTTON_DOWN, false);
SDL_SetEventEnabled((uint)SDL_EventType.SDL_EVENT_JOYSTICK_BUTTON_UP, false);
SDL_SetEventEnabled((uint)SDL_EventType.SDL_EVENT_GAMEPAD_SENSOR_UPDATE, false);
string gamepadDbPath = Path.Combine(AppDataManager.BaseDirPath, "SDL_GameControllerDB.txt");
if (File.Exists(gamepadDbPath))
{
SDL_AddGamepadMappingsFromFile(gamepadDbPath);
}
_registeredWindowHandlers = new ConcurrentDictionary<uint, Action<SDL_Event>>();
_worker = new Thread(EventWorker);
_isRunning = true;
_worker.Start();
}
}
public bool RegisterWindow(uint windowId, Action<SDL_Event> windowEventHandler)
{
return _registeredWindowHandlers.TryAdd(windowId, windowEventHandler);
}
public void UnregisterWindow(uint windowId)
{
_registeredWindowHandlers.Remove(windowId, out _);
}
private void HandleSDLEvent(ref SDL_Event evnt)
{
if (evnt.type == (uint)SDL_EventType.SDL_EVENT_GAMEPAD_ADDED)
{
var instanceId = evnt.jdevice.which;
Logger.Debug?.Print(LogClass.Application, $"Added joystick instance id {instanceId}");
OnJoyStickConnected?.Invoke(instanceId);
}
else if (evnt.type == (uint)SDL_EventType.SDL_EVENT_GAMEPAD_REMOVED)
{
var instanceId = evnt.jdevice.which;
Logger.Debug?.Print(LogClass.Application, $"Removed joystick instance id {instanceId}");
OnJoystickDisconnected?.Invoke(instanceId);
}
else if (evnt.type == (uint)SDL_EventType.SDL_EVENT_JOYSTICK_BATTERY_UPDATED)
{
OnJoyBatteryUpdated?.Invoke(evnt.jbattery.which, evnt.jbattery);
}
else if (evnt.type is >= (uint)SDL_EventType.SDL_EVENT_WINDOW_FIRST and <= (uint)SDL_EventType.SDL_EVENT_WINDOW_LAST
or (uint)SDL_EventType.SDL_EVENT_MOUSE_BUTTON_DOWN
or (uint)SDL_EventType.SDL_EVENT_MOUSE_BUTTON_UP)
{
if (_registeredWindowHandlers.TryGetValue(evnt.window.windowID, out Action<SDL_Event> handler))
{
handler(evnt);
}
}
}
private void EventWorker()
{
const int WaitTimeMs = 10;
using ManualResetEventSlim waitHandle = new(false);
while (_isRunning)
{
MainThreadDispatcher?.Invoke(() =>
{
while (SDL_PollEvent(out SDL_Event evnt))
{
HandleSDLEvent(ref evnt);
}
});
waitHandle.Wait(WaitTimeMs);
}
}
protected virtual void Dispose(bool disposing)
{
if (!disposing)
{
return;
}
lock (_lock)
{
if (_isRunning)
{
_refereceCount--;
if (_refereceCount == 0)
{
_isRunning = false;
_worker?.Join();
SDL_Quit();
OnJoyStickConnected = null;
OnJoystickDisconnected = null;
}
}
}
}
public void Dispose()
{
GC.SuppressFinalize(this);
Dispose(true);
}
}
}

View File

@@ -8,7 +8,7 @@ using LibHac.Ns;
using LibHac.Tools.FsSystem;
using Ryujinx.Audio.Backends.Dummy;
using Ryujinx.Audio.Backends.OpenAL;
using Ryujinx.Audio.Backends.SDL2;
using Ryujinx.Audio.Backends.SDL3;
using Ryujinx.Audio.Backends.SoundIo;
using Ryujinx.Audio.Integration;
using Ryujinx.Ava.Common;
@@ -587,11 +587,6 @@ namespace Ryujinx.Ava
return;
}
foreach (IGamepad gamepad in RyujinxApp.MainWindow.InputManager.GamepadDriver.GetGamepads())
{
gamepad?.ClearLed();
}
_isStopped = true;
Stop();
}
@@ -679,7 +674,7 @@ namespace Ryujinx.Ava
public async Task<bool> LoadGuestApplication(BlitStruct<ApplicationControlProperty>? customNacpData = null)
{
InitEmulatedSwitch();
InitializeSwitchInstance();
MainWindow.UpdateGraphicsConfig();
SystemVersion firmwareVersion = ContentManager.GetCurrentFirmwareVersion();
@@ -762,8 +757,6 @@ namespace Ryujinx.Ava
{
romFsFiles = Directory.GetFiles(ApplicationPath, "*.romfs");
}
Logger.Notice.Print(LogClass.Application, $"Loading unpacked content archive from '{ApplicationPath}'.");
if (romFsFiles.Length > 0)
{
@@ -790,8 +783,6 @@ namespace Ryujinx.Ava
}
else if (File.Exists(ApplicationPath))
{
Logger.Notice.Print(LogClass.Application, $"Loading content archive from '{ApplicationPath}'.");
switch (Path.GetExtension(ApplicationPath).ToLowerInvariant())
{
case ".xci":
@@ -894,7 +885,7 @@ namespace Ryujinx.Ava
Logger.Info?.Print(LogClass.Emulation, "Emulation was paused");
}
private void InitEmulatedSwitch()
private void InitializeSwitchInstance()
{
// Initialize KeySet.
VirtualFileSystem.ReloadKeySet();
@@ -966,7 +957,7 @@ namespace Ryujinx.Ava
{
var availableBackends = new List<AudioBackend>
{
AudioBackend.SDL2,
AudioBackend.SDL3,
AudioBackend.SoundIo,
AudioBackend.OpenAl,
AudioBackend.Dummy,
@@ -1005,7 +996,7 @@ namespace Ryujinx.Ava
deviceDriver = currentBackend switch
{
AudioBackend.SDL2 => InitializeAudioBackend<SDL2HardwareDeviceDriver>(AudioBackend.SDL2, nextBackend),
AudioBackend.SDL3 => InitializeAudioBackend<SDL3HardwareDeviceDriver>(AudioBackend.SDL3, nextBackend),
AudioBackend.SoundIo => InitializeAudioBackend<SoundIoHardwareDeviceDriver>(AudioBackend.SoundIo, nextBackend),
AudioBackend.OpenAl => InitializeAudioBackend<OpenALHardwareDeviceDriver>(AudioBackend.OpenAl, nextBackend),
_ => new DummyHardwareDeviceDriver(),
@@ -1049,7 +1040,7 @@ namespace Ryujinx.Ava
_viewModel.WindowState = WindowState.FullScreen;
}
if (_viewModel.WindowState is WindowState.FullScreen || _viewModel.StartGamesWithoutUI)
if (_viewModel.WindowState is WindowState.FullScreen)
{
_viewModel.ShowMenuAndStatusBar = false;
}
@@ -1156,8 +1147,8 @@ namespace Ryujinx.Ava
LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%",
dockedMode,
ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(),
Device.Statistics.FormatGameFrameRate(),
Device.Statistics.FormatFifoPercent(),
$"{Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)",
$"FIFO: {Device.Statistics.GetFifoPercent():00.00} %",
_displayCount));
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -1,341 +1,258 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.9.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 1000 1000.2" style="enable-background:new 0 0 1000 1000.2;" xml:space="preserve">
<style type="text/css">
.st0{fill:#00BBDB;stroke:#000000;}
.st1{fill:#333333;stroke:#000000;stroke-width:0.93;}
.st2{fill:#333333;stroke:#000000;stroke-width:0.96;}
.st3{fill:#333333;stroke:#000000;stroke-width:0.85;}
.st4{fill:#1A1A1A;stroke:#000000;}
.st5{fill:#333333;stroke:#000000;}
.st6{fill:#1A1A1A;stroke:#1A1A1A;}
.st7{fill:#1A1A1A;stroke:#4D4D4D;}
.st8{stroke:#4D4D4D;}
.st9{stroke:#000000;}
.st10{opacity:0.1;}
.st11{fill:#FF5F55;}
.st12{fill:#FF5F53;}
.st13{fill:#FFFFFF;}
.st14{fill:#999595;stroke:#000000;stroke-width:2.39;stroke-linecap:round;stroke-linejoin:round;}
.st15{fill:#3A3D40;stroke:#000000;stroke-width:2.73;stroke-linecap:round;stroke-linejoin:round;}
</style>
<g id="layer1">
<g id="g344">
<path id="path66-7" d="M349.1,906.6h-7.9c-3.6,0-6.4-2.9-6.5-6.5V71.2c0-3.6,2.9-6.4,6.5-6.5h7.9c3.6,0,6.4,2.9,6.5,6.5V207
c0,4.9-1.2,9.6-3.4,14l-6.7,13v79.2l6.7,13c2.2,4.3,3.4,9.1,3.4,13.9v269.7c0,4.9-1.2,9.6-3.4,14l-6.7,13V716l6.7,13
c2.2,4.3,3.4,9.1,3.4,13.9v157.2C355.6,903.7,352.7,906.6,349.1,906.6L349.1,906.6L349.1,906.6z M341.2,65.7c-3,0-5.5,2.4-5.5,5.5
v828.9c0,3,2.4,5.5,5.5,5.5h7.9c3,0,5.5-2.4,5.5-5.5V742.9c0-4.7-1.1-9.3-3.3-13.5l-6.8-13.1c0-0.1-0.1-0.2-0.1-0.2v-79.5
c0-0.1,0-0.2,0.1-0.2l6.8-13.1c2.2-4.2,3.3-8.8,3.3-13.5V340.1c0-4.7-1.1-9.3-3.3-13.5l-6.8-13.1c0-0.1-0.1-0.2-0.1-0.2v-79.5
c0-0.1,0-0.2,0.1-0.2l6.8-13.1c2.2-4.2,3.3-8.8,3.3-13.5V71.2c0-3-2.4-5.5-5.5-5.5L341.2,65.7L341.2,65.7z"/>
<path id="path68-5" d="M335.3,858.9h-11.2c-0.3,0-0.5-0.2-0.5-0.5V72c0-0.3,0.2-0.5,0.5-0.5h11.2c0.3,0,0.5,0.2,0.5,0.5v786.4
C335.8,858.7,335.6,858.9,335.3,858.9z M324.6,857.9h10.2V72.5h-10.2V857.9z"/>
<path id="path70-3" d="M318.1,1000H211.9C94.9,1000,0,905.2,0,788.1l0,0V220.9C0,104.1,95.1,9.1,211.9,9.1h106.2
c3.6,0,6.5,2.9,6.5,6.5v978C324.6,997.1,321.7,1000,318.1,1000L318.1,1000L318.1,1000z M211.9,10.1C95.6,10.1,1,104.7,1,220.9
v567.2C1,904.4,95.6,999,211.9,999h106.2c3,0,5.5-2.4,5.5-5.5v-978c0-3-2.4-5.5-5.5-5.5H211.9L211.9,10.1L211.9,10.1z"/>
<path id="path98" d="M349.1,717.1h-4.2c-0.6,0-1-0.4-1-1l0,0v-79.5c0-0.6,0.4-1,1-1h4.2c3.8,0,6.9,3.1,7,7v67.6
C356.1,714,353,717.1,349.1,717.1z M345.9,715.1h3.2c2.7,0,5-2.2,5-5v-67.6c0-2.7-2.2-5-5-5h-3.2V715.1z"/>
<path id="path100" d="M349.1,314.3h-4.2c-0.6,0-1-0.4-1-1v-79.5c0-0.6,0.4-1,1-1h4.2c3.8,0,6.9,3.1,7,7v67.6
C356.1,311.2,353,314.3,349.1,314.3z M345.9,312.3h3.2c2.7,0,5-2.2,5-5v-67.6c0-2.7-2.2-5-5-5h-3.2V312.3z"/>
<path id="path1144" class="st0" d="M193.2,997.9c-26.2-3-49.9-8.5-75.1-21C96.2,966,77,951.2,60.7,934.4
C28.4,901.1,7,856.6,1.7,807.7c-0.4-3.4-0.1-30.5-0.3-72.2C0.3,576.9,0.4,214.9,1.8,201c1.8-17.7,6.1-36.5,12-52.4
c2.8-7.5,7.1-15.8,10.7-23.1C55.9,61.8,116.8,21.2,187,12c18.2-2.4,133.1-3.3,135.5-0.9c0.1,0.1,0.9,2.3,1,7
c0.4,11.1,0.4,36,1.3,79.4c0.7,32.7,0,76.3,0.1,132.5c0.2,70.3-1.6,160.2-1.6,274.8c0,484.5,1.4,491.2-0.9,492.4
c-0.3,0.2-2.5,1.7-5.3,1.8c-21.9,1.1-119.2-0.5-120.6-0.7L193.2,997.9L193.2,997.9z"/>
<polygon id="polygon80" class="st1" points="173.9,448.1 160.6,470.2 187.3,470.2 "/>
<polygon id="polygon82" class="st2" points="187.1,605.6 174.3,627.3 161.5,605.6 "/>
<polygon id="polygon84" class="st1" points="105.9,524.7 84,538.1 105.9,551.5 "/>
<polygon id="polygon86" class="st3" points="266.4,537.9 240.8,551.6 240.8,524.1 "/>
<path id="path102" d="M17.3,139.3c-0.3,0-0.5-0.1-0.7-0.3l-3.4-3.4c-2-2-2.6-5.1-1.5-7.8C46.5,50.1,123.7,0.1,208.9,0h51.2
c3.8,0,6.9,3.1,7,7v2.6c0,0.6-0.4,1-1,1h-54.2C127.4,10.5,51.1,61,18.2,138.7c-0.1,0.3-0.4,0.5-0.7,0.6L17.3,139.3L17.3,139.3z
M208.9,2C124.5,2.1,48,51.7,13.5,128.7c-0.8,1.9-0.4,4.1,1.1,5.5l2.4,2.4C50.6,58.8,127.3,8.5,212,8.6h53.2V7c0-2.7-2.2-5-5-5
L208.9,2L208.9,2z"/>
<path id="path104" d="M295.6,116.1h-46.9c-2.2,0-4-1.8-4-4v-11.7c0-2.2,1.8-4,4-4h46.9c2.2,0,4,1.8,4,4v11.7
C299.6,114.3,297.8,116.1,295.6,116.1z M248.7,98.5c-1.1,0-2,0.9-2,2v11.7c0,1.1,0.9,2,2,2h46.9c1.1,0,2-0.9,2-2v-11.7
c0-1.1-0.9-2-2-2H248.7z"/>
<path id="path106" d="M173.9,502.9c-21.2,0-38.5-17.2-38.5-38.5s17.2-38.5,38.5-38.5s38.5,17.2,38.5,38.5S195.2,502.9,173.9,502.9
L173.9,502.9z M173.9,428c-20.1,0-36.5,16.3-36.5,36.5s16.3,36.5,36.5,36.5s36.5-16.3,36.5-36.5S194.1,428,173.9,428L173.9,428z"
/>
<path id="path108" d="M173.9,649.8c-21.2,0-38.5-17.2-38.5-38.5s17.2-38.5,38.5-38.5s38.5,17.2,38.5,38.5S195.2,649.8,173.9,649.8
L173.9,649.8z M173.9,574.9c-20.1,0-36.5,16.3-36.5,36.5s16.3,36.5,36.5,36.5s36.5-16.3,36.5-36.5l0,0
C210.4,591.2,194.1,574.9,173.9,574.9L173.9,574.9z"/>
<path id="path110" d="M247.4,576.3c-21.2,0-38.5-17.2-38.5-38.5s17.2-38.5,38.5-38.5s38.5,17.2,38.5,38.5l0,0
C285.8,559.1,268.6,576.3,247.4,576.3z M247.4,501.4c-20.1,0-36.5,16.3-36.5,36.5s16.3,36.5,36.5,36.5s36.5-16.3,36.5-36.5l0,0
C283.8,517.7,267.5,501.4,247.4,501.4L247.4,501.4L247.4,501.4z"/>
<path id="path112" d="M100.5,576.3c-21.2,0-38.5-17.2-38.5-38.5s17.2-38.5,38.5-38.5s38.5,17.2,38.5,38.5l0,0
C138.9,559.1,121.7,576.3,100.5,576.3z M100.5,501.4c-20.1,0-36.5,16.3-36.5,36.5s16.3,36.5,36.5,36.5s36.5-16.3,36.5-36.5l0,0
C136.9,517.7,120.6,501.4,100.5,501.4L100.5,501.4L100.5,501.4z"/>
<path id="path114" d="M250.1,753.7h-45c-5.5,0-9.9-4.4-9.9-9.9v-45c0-5.5,4.4-9.9,9.9-9.9h45c5.5,0,9.9,4.4,9.9,9.9v45
C260,749.3,255.5,753.7,250.1,753.7z M205.1,690.9c-4.4,0-7.9,3.6-7.9,7.9v45c0,4.4,3.6,7.9,7.9,7.9h45c4.4,0,7.9-3.6,7.9-7.9v-45
c0-4.4-3.6-7.9-7.9-7.9H205.1z"/>
<path id="path116" d="M227.6,741.7c-11.3,0-20.4-9.2-20.4-20.4s9.2-20.4,20.4-20.4s20.4,9.2,20.4,20.4l0,0
C248,732.6,238.9,741.7,227.6,741.7z M227.6,702.8c-10.2,0-18.4,8.3-18.4,18.4s8.3,18.4,18.4,18.4s18.4-8.3,18.4-18.4
S237.8,702.9,227.6,702.8z"/>
<path id="path118" d="M110.8,260.2H98.5c-0.6,0-1-0.4-1-1l0,0c1.7-39.6,33.4-71.3,73-73c0.3,0,0.5,0.1,0.7,0.3s0.3,0.4,0.3,0.7
v12.3c0,3.5-2.6,6.4-6,6.9c-24.7,3.7-44.2,23.1-47.9,47.9C117.2,257.6,114.3,260.2,110.8,260.2L110.8,260.2z M99.6,258.2h11.2
c2.5,0,4.6-1.8,4.9-4.3c3.9-25.6,24-45.7,49.6-49.6c2.5-0.3,4.3-2.4,4.3-4.9v-11.2C132.2,190.3,101.7,220.8,99.6,258.2L99.6,258.2
z"/>
<path id="path120" d="M170.6,338.9L170.6,338.9c-39.6-1.7-71.3-33.4-73-73c0-0.6,0.4-1,1-1h12.3c3.5,0,6.4,2.6,6.9,6
c3.7,24.7,23.1,44.2,47.9,47.9c3.4,0.5,6,3.4,6,6.9V338C171.6,338.5,171.1,338.9,170.6,338.9L170.6,338.9z M99.6,266.9
c2.2,37.4,32.6,67.8,70,70v-11.2c0-2.5-1.8-4.6-4.3-4.9c-25.6-3.9-45.7-24-49.6-49.6c-0.3-2.5-2.4-4.3-4.9-4.3H99.6L99.6,266.9z"
/>
<path id="path122" d="M177.3,338.9c-0.6,0-1-0.4-1-1v-12.3c0-3.5,2.6-6.4,6-6.9c24.7-3.7,44.2-23.1,47.9-47.9c0.5-3.4,3.4-6,6.9-6
h12.3c0.6,0,1,0.4,1,1l0,0C248.6,305.5,216.9,337.2,177.3,338.9L177.3,338.9L177.3,338.9z M237,266.9c-2.5,0-4.6,1.8-4.9,4.3
c-3.9,25.6-24,45.7-49.6,49.6c-2.5,0.3-4.3,2.4-4.3,4.9v11.2c37.4-2.2,67.8-32.6,70-70H237z"/>
<path id="path124" d="M249.3,260.2H237c-3.5,0-6.4-2.6-6.9-6c-3.7-24.7-23.1-44.2-47.9-47.9c-3.4-0.5-6-3.4-6-6.9v-12.3
c0-0.3,0.1-0.5,0.3-0.7s0.5-0.3,0.7-0.3c39.6,1.7,71.3,33.4,73,73C250.3,259.7,249.9,260.2,249.3,260.2L249.3,260.2L249.3,260.2z
M178.3,188.2v11.2c0,2.5,1.8,4.6,4.3,4.9c25.6,3.9,45.7,24,49.6,49.6c0.3,2.5,2.4,4.3,4.9,4.3h11.2
C246.1,220.8,215.6,190.3,178.3,188.2L178.3,188.2L178.3,188.2z"/>
<path id="path126" d="M173.9,339c-1.2,0-2.3,0-3.4-0.1c-0.5,0-0.9-0.5-0.9-1v-12.3c0-2.5-1.8-4.6-4.3-4.9
c-25.6-3.9-45.7-24-49.6-49.6c-0.3-2.5-2.4-4.3-4.9-4.3H98.5c-0.5,0-1-0.4-1-0.9c-0.1-1.1-0.1-2.2-0.1-3.4s0-2.3,0.1-3.4
c0-0.5,0.5-0.9,1-0.9h12.3c2.5,0,4.6-1.8,4.9-4.3c3.9-25.6,24-45.7,49.6-49.6c2.5-0.3,4.3-2.4,4.3-4.9v-12.3c0-0.5,0.4-1,0.9-1
c2.3-0.1,4.5-0.1,6.8,0c0.5,0,0.9,0.5,0.9,1v12.3c0,2.5,1.8,4.6,4.3,4.9c25.6,3.9,45.7,24,49.6,49.6c0.3,2.5,2.4,4.3,4.9,4.3h12.3
c0.5,0,1,0.4,1,0.9c0.1,1.1,0.1,2.2,0.1,3.4s0,2.3-0.1,3.4c0,0.5-0.5,0.9-1,0.9H237c-2.5,0-4.6,1.8-4.9,4.3
c-3.9,25.6-24,45.7-49.6,49.6c-2.5,0.3-4.3,2.4-4.3,4.9v12.3c0,0.5-0.4,1-0.9,1C176.2,339,175.1,339,173.9,339L173.9,339z
M171.6,337c1.5,0.1,3.1,0.1,4.7,0v-11.3c0-3.5,2.6-6.4,6-6.9c24.7-3.7,44.2-23.1,47.9-47.9c0.5-3.4,3.4-6,6.9-6h11.3v-4.6H237
c-3.5,0-6.4-2.6-6.9-6c-3.7-24.7-23.1-44.2-47.9-47.9c-3.4-0.5-6-3.4-6-6.9v-11.3c-1.5-0.1-3.1-0.1-4.7,0v11.3
c0,3.5-2.6,6.4-6,6.9c-24.7,3.7-44.2,23.1-47.9,47.9c-0.5,3.4-3.4,6-6.9,6H99.4v4.6h11.3c3.5,0,6.4,2.6,6.9,6
c3.7,24.7,23.1,44.2,47.9,47.9c3.4,0.5,6,3.4,6,6.9L171.6,337L171.6,337z"/>
<path id="path179" class="st4" d="M94,574.3c-14.7-3.2-24.9-12.7-29-27.1c-1-3.4-1.2-6.3-0.9-11.7c0.3-6.5,0.7-7.8,3.5-13.5
c11.4-22.9,40.7-27.9,58.6-10c19.4,19.4,12,51.9-13.9,60.8C107.1,574.5,98.5,575.3,94,574.3L94,574.3L94,574.3z M106.6,538
c0-7.3-0.2-13.4-0.4-13.4c-0.9,0-23.3,13.2-23,13.5c0.7,0.7,22.2,13.1,22.8,13.2C106.4,551.3,106.6,545.4,106.6,538L106.6,538
L106.6,538z"/>
<path id="path181" class="st4" d="M168.5,500.6c-6.9-0.9-15-5-20.5-10.4c-14.6-14.5-14.6-36.9,0.1-51.4c23-22.9,62.3-7,62.3,25.4
c0,10.4-3.5,18.8-10.7,26C191.3,498.4,180.4,502.1,168.5,500.6L168.5,500.6L168.5,500.6z M187.1,469.9
c-0.9-2.4-12.8-22.4-13.3-22.4c-0.4,0-5.9,9-12,19.9l-2,3.6h13.9C185.9,471,187.5,470.9,187.1,469.9L187.1,469.9L187.1,469.9z"/>
<path id="path185" class="st4" d="M165.2,647c-12.9-3.5-23.1-13.7-26.5-26.6c-5.9-22.7,11.6-45.2,35.2-45.3
c7.4,0,11.3,0.9,18.1,4.5c16.9,8.8,23.9,30.4,15.4,47.9C199.8,643,181.7,651.4,165.2,647L165.2,647L165.2,647z M181.4,616.9
c3.6-6.3,6.5-11.6,6.3-11.8s-6.4-0.3-13.8-0.1l-13.5,0.2l6.7,11.6c3.7,6.4,6.9,11.6,7.2,11.6C174.5,628.4,177.8,623.2,181.4,616.9
L181.4,616.9z"/>
<path id="path187" class="st4" d="M243.9,574.2c-0.3,0-1.6-0.3-2.9-0.5c-11.2-2.2-20.9-9.7-26.2-20.1c-2.8-5.6-4.2-12.2-3.8-17.9
c1.1-14.8,11.3-27.7,25.6-32.5c8.8-2.9,18-2.2,26.7,2.2s15.5,12.1,18.6,21.3c5.3,15.3-0.7,32.2-14.7,41.6c-6,4-13.4,6.3-20.2,6.2
C245.7,574.3,244.2,574.3,243.9,574.2L243.9,574.2L243.9,574.2z M243.2,551.1c0.7-0.4,3-1.6,5.1-2.8c8.7-4.8,15.6-9.2,16.2-10
c0.3-0.4,0.3-0.5,0-0.9c-0.9-1.3-21.5-13.5-22.9-13.5c-0.3,0-0.6,0.1-0.7,0.2c-0.4,0.5-0.6,4.8-0.6,13.9c0,9.1,0.2,13,0.6,13.5
C241.3,551.9,241.8,551.8,243.2,551.1L243.2,551.1L243.2,551.1z"/>
<path id="path189" class="st5" d="M224,739.1c-1-0.2-3-1-4.4-1.6c-2.1-1-2.9-1.6-4.8-3.5c-1.9-1.9-2.5-2.7-3.5-4.8
c-2.7-5.5-2.7-10.6,0-16.1c1-2.1,1.6-2.8,3.5-4.8c3.6-3.6,7.7-5.3,12.7-5.3c8.4,0,15.7,5.7,17.8,13.9c0.5,2,0.5,6.2,0,8.3
c-1.5,6.3-6.3,11.5-12.7,13.4C230.2,739.4,226.4,739.6,224,739.1L224,739.1L224,739.1z"/>
<path id="path191" class="st4" d="M203.2,751.3c-1.6-0.4-3-1.3-4.1-2.6c-1.8-2.1-1.7-0.9-1.7-27.4s-0.1-25.3,1.9-27.5
c0.6-0.7,1.7-1.5,2.5-1.9l1.5-0.7h48.3l1.8,0.9c1.3,0.6,2,1.2,2.7,2.1c1.8,2.3,1.7,1.3,1.6,27.8c-0.1,23.8-0.1,23.9-0.6,25.1
c-0.7,1.6-2.3,3.1-3.9,3.9l-1.3,0.7l-23.8,0C210,751.6,204.2,751.5,203.2,751.3L203.2,751.3L203.2,751.3z M230.5,741.6
c10.6-1.5,18.1-10.7,17.5-21.4c-0.2-3.4-0.7-5.3-2-8.1c-4.6-9.3-15.5-13.7-25.3-10.2c-2.8,1-4.7,2.2-7,4.3c-3,2.8-5.1,6.3-6,10.3
c-0.5,2.2-0.5,7.2,0,9.4c0.9,3.9,3,7.4,6,10.2c2.2,2.1,4.2,3.3,6.7,4.3C223.7,741.7,227.1,742.1,230.5,741.6L230.5,741.6
L230.5,741.6z"/>
<path id="path1082" class="st6" d="M171.8,330.4c0-6-0.1-6.5-0.7-7.9c-1.1-2.1-2.9-3.4-5.6-3.9c-15.1-2.7-27.5-10.1-36.5-21.9
c-5-6.5-8.8-15.1-10.4-23.2c-0.8-4.3-1.4-5.7-3-7c-1.8-1.5-3.3-1.8-10-1.8h-5.9v-4.2h6.3c6.1,0,6.3,0,8-0.9
c2.4-1.2,3.3-2.7,4.2-7.3c4.6-23.2,22.4-41,45.6-45.4c4.2-0.8,5.6-1.6,6.9-3.8c0.9-1.5,0.9-1.6,1-8.1l0.1-6.6h4.2v6.3
c0,6.2,0,6.3,0.9,8c1.3,2.6,2.6,3.4,7.6,4.4c19.4,3.8,35.1,17,42.3,35.4c1,2.7,1.9,5.9,3.3,12c0.6,2.6,1.7,4.2,3.6,5.2
c1.1,0.6,2,0.7,7.9,0.8l6.6,0.1v4.2h-6.3c-6.1,0-6.3,0-8,0.9c-2.2,1.1-3.4,2.9-3.9,5.9c-4.1,23.8-22.2,42.3-46,46.8
c-4.2,0.8-5.7,1.7-7,4.1c-0.8,1.5-0.8,1.9-0.9,8l-0.1,6.4h-4.2L171.8,330.4L171.8,330.4z"/>
<path id="path1084" class="st7" d="M99.8,257.3c0.1-0.5,0.3-1.9,0.4-3.2c0.4-3.9,1.7-10.1,3.3-14.8c8-24.1,28.1-42.7,52.7-48.9
c3.1-0.8,7.6-1.6,11.3-1.9l2-0.2v5.9c0,8.5-0.4,9.1-6.5,10.4c-18.4,3.7-33.7,15.2-41.9,31.9c-2.6,5.1-4.1,10.1-5.5,17
c-0.5,2.4-1.8,4-3.7,4.4c-0.7,0.2-3.8,0.3-6.8,0.3h-5.5L99.8,257.3L99.8,257.3z"/>
<path id="path1086" class="st7" d="M165.8,336.4c-27-3-50.8-21.4-60.9-46.9c-2.4-6-4.2-13.4-4.7-18.7c-0.1-1.3-0.3-2.7-0.4-3.1
l-0.2-0.8l6.3,0.1c5.8,0.1,6.4,0.2,7.2,0.8c1.6,1.2,2,2.2,2.9,6.2c1.2,5.8,2.4,9.4,5.2,14.8c2.9,5.7,5.7,9.8,9.9,14.2
c8.6,9.1,19.7,15.2,31.7,17.5c3.6,0.7,4.6,1.2,5.8,2.7c0.6,0.8,0.7,1.4,0.8,7.2c0.1,5.9,0.1,6.3-0.5,6.3
C168.6,336.7,167.2,336.6,165.8,336.4L165.8,336.4L165.8,336.4z"/>
<path id="path1088" class="st7" d="M235.7,257.7c-2.4-0.9-2.8-1.6-3.8-6.5c-3.7-18.2-15.4-33.5-32-41.6c-4.8-2.4-8.6-3.7-13.5-4.7
c-4.7-1-6.2-1.6-7.1-2.9c-0.7-1.1-0.7-1.4-0.7-7.4c0-3.5,0.1-6.3,0.3-6.3s1.9,0.2,4,0.5c12,1.5,22.9,5.6,32.8,12.4
c4.4,3,7.4,5.6,11.6,9.9c9.3,9.6,15.5,20.7,18.8,33.5c0.9,3.6,2.1,10.9,2.1,12.9c0,0.6-0.3,0.6-5.6,0.6
C238.7,258.1,236.5,258,235.7,257.7L235.7,257.7L235.7,257.7z"/>
<path id="path1090" class="st7" d="M178.3,330.5c0.1-5.8,0.2-6.4,0.8-7.2c1.2-1.6,2.2-2,6.3-2.9c23.5-4.7,41.9-23.3,46.4-47
c0.7-3.5,1.1-4.5,2.7-5.6c0.8-0.6,1.4-0.7,7.2-0.8l6.3-0.1l-0.2,1.7c-1.2,10.5-3.3,18-7.5,26.4c-11.2,22.6-32.9,38.1-58,41.3
c-2.1,0.3-3.9,0.5-4,0.5S178.3,333.9,178.3,330.5L178.3,330.5L178.3,330.5z"/>
<path id="path1092" class="st4" d="M247.6,113.5c-0.5-0.6-0.7-1.8-0.7-7.2c0-4.5,0.2-6.7,0.5-7c0.4-0.4,6.4-0.5,24.6-0.5
c21.3,0,24.2,0.1,24.8,0.7c0.5,0.6,0.7,1.8,0.7,7c0,6.1,0,6.4-1,7c-0.8,0.6-3.9,0.7-24.6,0.7S248.2,114,247.6,113.5z"/>
<path id="path1094" class="st8" d="M15,134.2c-2.3-2.8-2.2-3.7,2.3-12.6C28,99.9,40.9,82,57.8,65.1C89.1,33.7,129.4,12.8,173,5.3
c15.1-2.6,16.5-2.7,54-2.9l34.8-0.2l1.4,1.2c1,0.9,1.4,1.7,1.6,3l0.2,1.8l-33.8,0.2C195,8.6,192.5,8.8,178,11.1
c-38.4,6.2-72.9,22-103.3,47.3c-6.8,5.7-19.5,18.3-25.2,25.2c-8.4,10-16.5,21.7-22.5,32.4c-3.1,5.4-9.6,18.7-9.6,19.5
C17.3,136.4,16.5,136,15,134.2L15,134.2L15,134.2z"/>
<path id="path1150" class="st9" d="M334.4,616c0-216,0-634,0.1-526c0.1,108,0.3,614.3,0.1,722.4c0,29.2-0.1,43.2-0.1,45
C334.5,862.4,334.5,773.6,334.4,616L334.4,616L334.4,616z"/>
<path id="path1152" class="st4" d="M336.6,903.2c-1.5-2.1-0.8-44.2-0.8-417.6S335,70.3,336.5,68.2c1.4-2,3.4-2.2,8.5-2.2
c1.8,0,3.8-0.1,5.4,0.2c1.4,0.3,3.2,2,3.5,2.4c0.5,0.5,0.3,4.3,0.4,16.4c0.1,11.3-0.2,29-0.2,55.3c0,44.7,0.6,61.8,0.1,69.8
c-0.3,4.4-1.2,6.1-1.9,8.1c-1.2,3.1-3.9,8.2-5.6,10.9l-3.2,5l0.4,40.4c0.4,37.6-0.2,39,1.6,41.7c1.1,1.5,3,6.1,4.6,9.3l3,5.8
l1.3,7.9l-0.1,19.8l-0.1,15.8l-0.2,97.9c-0.2,93.1,1.2,126.7-0.3,141.1c-0.4,4.2-1.6,6.5-2.3,8.5c-0.9,2.7-2.1,4.9-3.9,7.8
l-3.7,6.1l0.4,15.6l0.1,25.1c0.1,26.5,0.6,40.4,1.3,40.9c0.6,0.4,2.7,4,4.3,8.5l3.3,9l1.1,9l-0.2,15.9l-0.2,57.9
c-0.2,61.9,0.4,84.5-0.5,85.5c-0.8,0.9-4.5,2.4-8.4,2.4C340.2,905.8,338,905.1,336.6,903.2L336.6,903.2L336.6,903.2z"/>
<path id="path1154" class="st4" d="M325.2,464.9V72.7h8.5v392.2l1.1,393l-5.3,0.2l-4.9-0.2L325.2,464.9L325.2,464.9z"/>
<path id="path1156" class="st0" d="M345.9,273.3v-38.6h3c1.7,0,3.6,0.9,4.3,2.1c1.7,2.8,1.7,70.3,0,73.1c-0.7,1.2-2.6,2.1-4.3,2.1
h-3V273.3L345.9,273.3z"/>
<path id="path1158" class="st0" d="M345.9,676.2v-38.7h3c1.8,0,3.6,0.9,4.3,2.1c1.7,2.8,1.7,70.3,0,73.2c-0.7,1.2-2.6,2.1-4.3,2.1
h-3V676.2L345.9,676.2z"/>
</g>
<g id="g315">
<g id="g64" class="st10">
<path id="path36" class="st11" d="M654.6,233.9v79.5h-4.2c-3.3,0-6-2.7-6-6v-67.6c0-3.3,2.7-6,6-6h4.2V233.9z"/>
<path id="path38" class="st11" d="M654.6,636.6v79.5h-4.2c-3.3,0-6-2.7-6-6v-67.6c0-3.3,2.7-6,6-6L654.6,636.6L654.6,636.6z"/>
<path id="path40" class="st11" d="M985.7,134.9l-3.4,3.4C949.1,60.3,872.5,9.6,787.6,9.6h-54.2V7c0-3.3,2.7-6,6-6h51.2
C875.4,1,952.3,50.8,987,128.2C988,130.5,987.5,133.1,985.7,134.9L985.7,134.9L985.7,134.9z"/>
<path id="path42" class="st11" d="M736.2,94.5V82.8c0-1.6-1.3-3-3-3h-11.7c-1.6,0-3,1.3-3,3l0,0v11.7c0,1.6-1.3,3-3,3h-11.7
c-1.6,0-3,1.3-3,3l0,0v11.7c0,1.6,1.3,3,3,3h11.7c1.6,0,3,1.3,3,3l0,0v11.7c0,1.6,1.3,3,3,3h11.7c1.6,0,3-1.3,3-3l0,0v-11.7
c0-1.6,1.3-3,3-3h11.7c1.6,0,3-1.3,3-3l0,0v-11.7c0-1.6-1.3-3-3-3h-11.7C737.5,97.5,736.2,96.1,736.2,94.5L736.2,94.5z"/>
<circle id="circle44" class="st11" cx="825.6" cy="333.9" r="37.5"/>
<circle id="circle46" class="st11" cx="825.6" cy="187.1" r="37.5"/>
<circle id="circle48" class="st11" cx="899" cy="260.5" r="37.5"/>
<circle id="circle50" class="st11" cx="752.2" cy="260.5" r="37.5"/>
<circle id="circle52" class="st11" cx="771.7" cy="721.3" r="27.9"/>
<path id="path54" class="st11" d="M822.3,460.3v12.3c0,3-2.2,5.5-5.2,5.9c-25.2,3.7-45,23.5-48.7,48.7c-0.4,2.9-2.9,5.1-5.9,5.2
h-12.3C751.9,493.3,783.2,462,822.3,460.3L822.3,460.3L822.3,460.3z"/>
<path id="path56" class="st11" d="M822.3,598.8v12.3c-39.1-1.7-70.3-33-72.1-72h12.3c3,0,5.5,2.2,5.9,5.2
c3.7,25.2,23.5,45,48.7,48.7C820.1,593.3,822.3,595.8,822.3,598.8L822.3,598.8L822.3,598.8z"/>
<path id="path58" class="st11" d="M901,539c-1.7,39.1-33,70.3-72.1,72v-12.3c0-3,2.2-5.5,5.2-5.9c25.2-3.7,45-23.5,48.7-48.7
c0.4-2.9,2.9-5.1,5.9-5.2L901,539L901,539z"/>
<path id="path60" class="st11" d="M901,532.4h-12.3c-3,0-5.5-2.2-5.9-5.2c-3.7-25.2-23.5-45-48.7-48.7c-2.9-0.4-5.1-2.9-5.2-5.9
v-12.3C868,462,899.3,493.3,901,532.4L901,532.4L901,532.4z"/>
<path id="path62" class="st11" d="M901.1,535.7c0,1.1,0,2.2-0.1,3.3h-12.3c-3,0-5.5,2.2-5.9,5.2c-3.7,25.2-23.5,45-48.7,48.7
c-2.9,0.4-5.1,2.9-5.2,5.9v12.3c-1.1,0.1-2.2,0.1-3.3,0.1s-2.2,0-3.3-0.1v-12.3c0-3-2.2-5.5-5.2-5.9c-25.2-3.7-45-23.5-48.7-48.7
c-0.4-2.9-2.9-5.1-5.9-5.2h-12.3c-0.1-1.1-0.1-2.2-0.1-3.3s0-2.2,0.1-3.3h12.3c3,0,5.5-2.2,5.9-5.2c3.7-25.2,23.5-45,48.7-48.7
c2.9-0.4,5.1-2.9,5.2-5.9v-12.3c1.1-0.1,2.2-0.1,3.3-0.1s2.2,0,3.3,0.1v12.3c0,3,2.2,5.5,5.2,5.9c25.2,3.7,45,23.5,48.7,48.7
c0.4,2.9,2.9,5.1,5.9,5.2H901C901,533.5,901.1,534.6,901.1,535.7L901.1,535.7z"/>
</g>
<path id="path72" d="M658.3,906.6h-7.9c-3.6,0-6.4-2.9-6.5-6.5V742.9c0-4.9,1.2-9.6,3.4-13.9l6.7-13v-79.2l-6.7-13
c-2.2-4.3-3.4-9.1-3.4-14V340.1c0-4.9,1.2-9.6,3.4-13.9l6.7-13V234l-6.7-13c-2.2-4.3-3.4-9.1-3.4-14V71.2c0-3.6,2.9-6.4,6.5-6.5
h7.9c3.6,0,6.4,2.9,6.5,6.5c1.1,276.3,1.2,552.6,0,828.9C664.7,903.7,661.8,906.6,658.3,906.6L658.3,906.6L658.3,906.6z
M650.4,65.7c-3,0-5.5,2.4-5.5,5.5V207c0,4.7,1.1,9.3,3.3,13.5l6.8,13.1c0,0.1,0.1,0.1,0.1,0.2v79.5c0,0.1,0,0.2-0.1,0.2
l-6.8,13.1c-2.2,4.2-3.3,8.8-3.3,13.5v269.7c0,4.7,1.1,9.3,3.3,13.5l6.8,13.1c0,0.1,0.1,0.1,0.1,0.2v79.5c0,0.1,0,0.2-0.1,0.2
l-6.8,13.1c-2.2,4.2-3.3,8.8-3.3,13.5v157.2c0,3,2.4,5.5,5.5,5.5h7.9c3,0,5.5-2.4,5.5-5.5V71.2c0-3-2.4-5.5-5.5-5.5L650.4,65.7
L650.4,65.7z"/>
<path id="path74" d="M675.5,858.9h-11.3c-0.3,0-0.5-0.2-0.5-0.5l0,0L664.5,72c0-0.3,0.2-0.5,0.5-0.5h11.3c0.3,0-0.3,0.2-0.3,0.5
v786.4C676,858.7,675.8,858.9,675.5,858.9z M665.6,857.9h9.4l0.8-785.4h-10.3l1,8.8c-0.6,256.1,4.5,511.8-1.1,768.3
C665.3,852.3,665.6,855.1,665.6,857.9L665.6,857.9z"/>
<path id="path76" d="M787.4,1000.2H681.2c-3.6,0-6.5-2.9-6.5-6.5v-978c0-3.6,2.9-6.5,6.5-6.5h106.2
c116.8,0,211.9,95.1,211.9,211.9v567.2C999.3,905.3,904.5,1000.2,787.4,1000.2L787.4,1000.2z M681.2,10.4c-3,0-5.5,2.4-5.5,5.5
v978c0,3,2.4,5.5,5.5,5.5h106.2c116.3,0,210.9-94.6,210.9-210.9V221.1c0-116.3-94.6-210.9-210.9-210.9L681.2,10.4z"/>
<path id="path128" d="M654.6,314.3h-4.2c-3.8,0-6.9-3.1-7-7v-67.6c0-3.8,3.1-6.9,7-7h4.2c0.6,0,1,0.4,1,1l0,0v79.5
C655.6,313.9,655.1,314.3,654.6,314.3L654.6,314.3z M650.4,234.8c-2.7,0-5,2.2-5,5v67.6c0,2.7,2.2,5,5,5h3.2v-77.5h-3.2V234.8z"/>
<path id="path130" d="M654.6,717.1h-4.2c-3.8,0-6.9-3.1-7-7v-67.6c0-3.8,3.1-6.9,7-7h4.2c0.6,0,1,0.4,1,1l0,0V716
C655.6,716.6,655.1,717.1,654.6,717.1L654.6,717.1z M650.4,637.6c-2.7,0-5,2.2-5,5v67.6c0,2.7,2.2,5,5,5h3.2v-77.5L650.4,637.6
L650.4,637.6z"/>
<path id="path1240" class="st12" d="M805.5,998.7c26.6-2.4,50.5-9.2,75.9-21.8c5.1-2.5,10-5.2,14.9-8
c19.1-11.3,36.3-27.1,49.6-41.7c25.5-28,45.5-70.1,51.4-116.5c0.1-0.7,0.4-4.3,0.4-6.8c-0.1-6.9,0.7-20,0.8-38.3
c0.7-77.5,1.1-244,1.1-376.4c0-101.2-0.3-182.5-1-188.9c-2.7-26.1-9.3-49.1-20.8-72.1c-31.8-63.9-93-107.4-164-116.7
c-11.4-1.5-60.7-1.6-96.6-1.3c-7.7,0.1-14.1-0.1-20.1,0.1c-7.5,0.2-12.9-0.1-16.2,0.2c-1.7,0.2-3,0.8-3.2,1
c-0.1,0.1-1.4,1.4-1.5,3.3c-0.3,5.2-0.3,17.1-0.4,38c0,4.2-0.1,8.8-0.1,13.8c0,4.9,0.1,10.1,0.1,15.8c-0.1,8.8,0.1,18.6,0.1,29.2
c0,9.8-0.2,20.5,0,32c0.2,9,0.2,18.4,0.2,28.4c-0.1,15.1,0.6,31.1,0.3,49c-0.2,15,0.1,30,0,46.9c-0.1,11.1,0.1,22.6,0,34.6
c-0.1,7.2,0,14.5-0.1,22.1c-0.5,52.1,0,112.2,0,180.3c0,254.3-0.3,376.1,0,435.2c0,7.7-0.2,14.5-0.1,20.2c0,2.2,0,4.3,0,6.2
c0,3.3,0,6.6,0,9.3c0,2.9-0.1,5.8,0,8c0.1,5,0.1,8.1,0.3,10c0.3,2.8,1.3,3.6,1.6,3.8c0.2,0.2,1.3,0.9,3.1,1.1
c4.5,0.4,14.6,0.3,27.1,0.2c9.9-0.1,21.3-0.2,32.8-0.2C772.7,998.8,805.8,999,805.5,998.7L805.5,998.7L805.5,998.7z"/>
<path id="path78" d="M771.7,763.2c-23.1,0-41.9-18.7-41.9-41.9s18.7-41.9,41.9-41.9c23.2,0,41.9,18.7,41.9,41.9l0,0
C813.5,744.4,794.8,763.1,771.7,763.2z M771.7,680.4c-22.6,0-40.9,18.3-40.9,40.9s18.3,40.9,40.9,40.9c22.6,0,40.9-18.3,40.9-40.9
l0,0C812.5,698.7,794.3,680.4,771.7,680.4z"/>
<path id="path88" class="st13" d="M838.9,203.2h-5.5l-7.6-10.9l-8,10.9h-5.4l10.6-16.3l-9.8-15.6h5.2l7.4,10.6l7.3-10.6h5
l-9.8,15.4L838.9,203.2L838.9,203.2z"/>
<path id="path90" class="st13" d="M765.9,244.5l-11.6,20.6v11.4h-4.4V265l-11.6-20.5h5.3l6.4,11.7l2.1,3l2.4-2.6l6.4-12.1H765.9
L765.9,244.5z"/>
<path id="path92" class="st13" d="M912.6,276.5h-4.7l-2.2-7l-12.4,0.3l-3.2,6.7h-4.5l9.9-35.2l6.7,3.2L912.6,276.5z M903.9,265.2
l-4.9-14.6l-4.6,14.8L903.9,265.2L903.9,265.2z"/>
<path id="path132" d="M982.3,139.3h-0.2c-0.3-0.1-0.6-0.3-0.7-0.6C948.4,61,872.1,10.5,787.6,10.6h-54.2c-0.6,0-1-0.4-1-1l0,0V7
c0-3.8,3.1-6.9,7-7h51.2C875.8,0.1,953,50.1,987.9,127.8c1.2,2.6,0.6,5.7-1.5,7.8L983,139C982.8,139.2,982.5,139.3,982.3,139.3
L982.3,139.3z M734.4,8.6h53.2c84.7-0.1,161.3,50.2,195,128l2.4-2.4l0,0c1.5-1.4,1.9-3.6,1.1-5.5C951.5,51.7,875,2.1,790.6,2
h-51.2c-2.7,0-5,2.2-5,5V8.6z"/>
<path id="path134" d="M733.2,133.7h-11.7c-2.2,0-4-1.8-4-4V118c0-1.1-0.9-2-2-2h-11.7c-2.2,0-4-1.8-4-4v-11.7c0-2.2,1.8-4,4-4
h11.7c1.1,0,2-0.9,2-2V82.8c0-2.2,1.8-4,4-4h11.7c2.2,0,4,1.8,4,4v11.7c0,1.1,0.9,2,2,2h11.7c2.2,0,4,1.8,4,4v11.7
c0,2.2-1.8,4-4,4h-11.7c-1.1,0-2,0.9-2,2v11.7C737.2,131.9,735.4,133.7,733.2,133.7z M703.9,98.5c-1.1,0-2,0.9-2,2v11.7
c0,1.1,0.9,2,2,2h11.7c2.2,0,4,1.8,4,4v11.7c0,1.1,0.9,2,2,2h11.7c1.1,0,2-0.9,2-2v-11.7c0-2.2,1.8-4,4-4H751c1.1,0,2-0.9,2-2
v-11.7c0-1.1-0.9-2-2-2h-11.7c-2.2,0-4-1.8-4-4V82.8c0-1.1-0.9-2-2-2h-11.7c-1.1,0-2,0.9-2,2v11.7c0,2.2-1.8,4-4,4H703.9z"/>
<path id="path136" d="M825.6,372.4c-21.2,0-38.5-17.2-38.5-38.5s17.2-38.5,38.5-38.5c21.3,0,38.5,17.2,38.5,38.5
C864,355.2,846.8,372.4,825.6,372.4z M825.6,297.5c-20.1,0-36.5,16.3-36.5,36.5s16.3,36.5,36.5,36.5c20.2,0,36.5-16.3,36.5-36.5
C862,313.8,845.7,297.5,825.6,297.5z"/>
<path id="path138" d="M825.6,225.5c-21.2,0-38.5-17.2-38.5-38.5s17.2-38.5,38.5-38.5c21.3,0,38.5,17.2,38.5,38.5
C864,208.3,846.8,225.5,825.6,225.5z M825.6,150.6c-20.1,0-36.5,16.3-36.5,36.5s16.3,36.5,36.5,36.5c20.2,0,36.5-16.3,36.5-36.5
C862,166.9,845.7,150.6,825.6,150.6z"/>
<path id="path140" d="M899,299c-21.2,0-38.5-17.2-38.5-38.5S877.7,222,899,222c21.3,0,38.5,17.2,38.5,38.5S920.3,298.9,899,299z
M899,224c-20.1,0-36.5,16.3-36.5,36.5S878.8,297,899,297c20.2,0,36.5-16.3,36.5-36.5S919.2,224,899,224z"/>
<path id="path142" d="M752.2,299c-21.2,0-38.5-17.2-38.5-38.5s17.2-38.5,38.5-38.5c21.3,0,38.5,17.2,38.5,38.5
C790.6,281.7,773.4,298.9,752.2,299z M752.2,224c-20.1,0-36.5,16.3-36.5,36.5s16.2,36.2,36.4,36.2s36.6-16,36.6-36.2
C788.6,240.4,772.3,224,752.2,224z"/>
<path id="path144" d="M771.7,750.2c-16,0-28.9-13-28.9-28.9s13-28.9,28.9-28.9s28.9,13,28.9,28.9l0,0
C800.6,737.3,787.7,750.2,771.7,750.2z M771.7,694.3c-14.9,0-26.9,12.1-26.9,26.9s12.1,26.9,26.9,26.9s26.9-12.1,26.9-26.9
S786.6,694.4,771.7,694.3z"/>
<path id="path146" d="M762.5,533.4h-12.3c-0.6,0-1-0.4-1-1l0,0c1.7-39.6,33.4-71.3,73-73c0.3,0,0.5,0.1,0.7,0.3
c0.2,0.2,0.3,0.4,0.3,0.7v12.3c0,3.5-2.6,6.4-6,6.9c-24.7,3.7-44.2,23.1-47.9,47.9C768.9,530.8,766,533.4,762.5,533.4z
M751.3,531.4h11.2c2.5,0,4.6-1.8,4.9-4.3c3.9-25.6,24-45.7,49.6-49.6c2.5-0.3,4.3-2.4,4.3-4.9v-11.2
C783.9,463.5,753.4,494,751.3,531.4z"/>
<path id="path148" d="M822.3,612.1C822.3,612.1,822.2,612.1,822.3,612.1c-39.6-1.7-71.3-33.4-73-73c0-0.6,0.4-1,1-1h12.3
c3.5,0,6.4,2.6,6.9,6c3.7,24.7,23.1,44.2,47.9,47.9c3.4,0.5,6,3.4,6,6.9v12.3C823.3,611.6,822.8,612.1,822.3,612.1L822.3,612.1
L822.3,612.1z M751.3,540c2.2,37.4,32.6,67.8,70,70v-11.2c0-2.5-1.8-4.6-4.3-4.9c-25.6-3.9-45.7-24-49.6-49.6
c-0.3-2.5-2.4-4.3-4.9-4.3H751.3z"/>
<path id="path150" d="M828.9,612.1c-0.6,0-1-0.4-1-1l0,0v-12.3c0-3.5,2.6-6.4,6-6.9c24.7-3.7,44.2-23.1,47.9-47.9
c0.5-3.4,3.4-6,6.9-6H901c0.6,0,1,0.4,1,1l0,0C900.2,578.7,868.6,610.3,828.9,612.1C829,612.1,829,612.1,828.9,612.1L828.9,612.1
L828.9,612.1z M888.7,540c-2.5,0-4.6,1.8-4.9,4.3c-3.9,25.6-24,45.7-49.6,49.6c-2.5,0.3-4.3,2.4-4.3,4.9V610
c37.4-2.2,67.8-32.6,70-70H888.7z"/>
<path id="path152" d="M901,533.4h-12.3c-3.5,0-6.4-2.6-6.9-6c-3.7-24.7-23.1-44.2-47.9-47.9c-3.4-0.5-6-3.4-6-6.9v-12.3
c0-0.3,0.1-0.5,0.3-0.7c0.2-0.2,0.5-0.3,0.7-0.3c39.6,1.7,71.3,33.4,73,73C902,532.9,901.6,533.3,901,533.4L901,533.4L901,533.4z
M829.9,461.4v11.2c0,2.5,1.8,4.6,4.3,4.9c25.6,3.9,45.7,24,49.6,49.6c0.3,2.5,2.4,4.3,4.9,4.3h11.2
C897.8,494,867.3,463.5,829.9,461.4z"/>
<path id="path154" d="M825.6,612.2c-1.2,0-2.3,0-3.4-0.1c-0.5,0-0.9-0.5-0.9-1v-12.3c0-2.5-1.8-4.6-4.3-4.9
c-25.6-3.9-45.7-24-49.6-49.6c-0.3-2.5-2.4-4.3-4.9-4.3h-12.3c-0.5,0-1-0.4-1-0.9c-0.1-1.1-0.1-2.2-0.1-3.4s0-2.3,0.1-3.4
c0-0.5,0.5-0.9,1-0.9h12.3c2.5,0,4.6-1.8,4.9-4.3c3.9-25.6,24-45.7,49.6-49.6c2.5-0.3,4.3-2.4,4.3-4.9v-12.3c0-0.5,0.4-1,0.9-1
c2.3-0.1,4.5-0.1,6.8,0c0.5,0,0.9,0.5,0.9,1v12.3c0,2.5,1.8,4.6,4.3,4.9c25.6,3.9,45.7,24,49.6,49.6c0.3,2.5,2.4,4.3,4.9,4.3H901
c0.5,0,1,0.4,1,0.9c0.1,1.1,0.1,2.2,0.1,3.4s0,2.3-0.1,3.4c0,0.5-0.5,0.9-1,0.9h-12.3c-2.5,0-4.6,1.8-4.9,4.3
c-3.9,25.6-24,45.7-49.6,49.6c-2.5,0.3-4.3,2.4-4.3,4.9v12.3c0,0.5-0.4,1-0.9,1C827.9,612.1,826.8,612.2,825.6,612.2L825.6,612.2
L825.6,612.2z M823.3,610.1c1.5,0.1,3.1,0.1,4.7,0v-11.3c0-3.5,2.6-6.4,6-6.9c24.7-3.7,44.2-23.1,47.9-47.9c0.5-3.4,3.4-6,6.9-6
h11.3v-4.6h-11.4c-3.5,0-6.4-2.6-6.9-6c-3.7-24.7-23.1-44.2-47.9-47.9c-3.4-0.5-6-3.4-6-6.9v-11.3c-1.5-0.1-3.1-0.1-4.7,0v11.3
c0,3.5-2.6,6.4-6,6.9c-24.7,3.7-44.2,23.1-47.9,47.9c-0.5,3.4-3.4,6-6.9,6h-11.3v4.6h11.3c3.5,0,6.4,2.6,6.9,6
c3.7,24.7,23.1,44.2,47.9,47.9c3.4,0.5,6,3.4,6,6.9L823.3,610.1L823.3,610.1L823.3,610.1z"/>
<path id="path1462" class="st4" d="M743,295.5c-6.3-1.7-11.3-3.9-16.5-9.1c-7.7-7.7-11.3-17-10.8-27.7c0.6-13.5,8-25,20-31
c10.5-5.2,22.6-5.3,32.7-0.3c7.9,3.9,12.8,10,16.6,18c5.2,10.8,4.2,24.4-2.4,34.6c-5,7.6-13.5,13.7-22,15.7
C755.7,296.8,747.8,296.7,743,295.5L743,295.5L743,295.5z"/>
<path id="path1464" class="st4" d="M819.4,222.9c0-0.3-3.7-0.9-5.7-1.6c-3-1.1-5.9-2.6-8-4c-6.9-4.6-11.7-10.9-14.5-18.9
c-1.3-3.8-2.1-5.2-2.1-11.4s0.7-7.2,2.1-11c3.9-11.1,11.8-19.1,22.8-23c4.2-1.5,5.2-2.3,11.6-2.3c6.4,0,7.3,0.8,11.6,2.3
c14.5,5.2,24,17.6,24.7,32.9c0.5,10.6-2.7,19.5-10.2,26.9c-7.6,7.5-14.4,10.7-24.6,11C823.6,223.7,820.2,223,819.4,222.9
L819.4,222.9L819.4,222.9z"/>
<path id="path1466" class="st4" d="M891.7,296.5c-6.7-1-15.6-5.8-20.8-12.9c-3-4.1-6.2-10.7-7.3-15.7
c-2.8-13.2,2.6-27.9,13.3-35.9c20-15.1,48.1-6.7,56.4,17c1.9,5.3,3,13.3,1.7,19.2c-1.5,6.8-4.9,12.9-10.2,18.2
c-5.3,5.2-9.9,8-16.4,9.6C903.5,297.2,896.3,297.2,891.7,296.5L891.7,296.5L891.7,296.5z"/>
<path id="path1468" class="st4" d="M820.4,369.9c-8.1-1.3-14.2-4-20.6-10.3c-3.8-3.7-5.4-6.2-7.2-10c-7.6-15.8-3-33.1,10.8-44.1
c6.4-5.1,13.6-7.6,22.1-7.6c29.9,0,46.7,33.8,28.7,58c-2.8,3.8-6.3,7.5-10.5,9.8C836.7,369.8,828.1,371.1,820.4,369.9L820.4,369.9
L820.4,369.9z"/>
<path id="path1500" class="st6" d="M823.6,602.7c-0.3-8.9-0.8-9.7-10.1-11.9c-21.1-4.8-37.2-20.1-42.6-41.3
c-0.6-2.2-1.1-5.2-1.1-5.9s-1-2.2-2.2-3.5l-2.3-2l-6.9-0.3h-7v-4h5.9c6.6,0,9.9-1,11.2-3.4c0.4-0.8,1.5-3.9,2-7
c2.7-16.3,14.9-30.7,29.5-38c4.7-2.4,11.4-4.6,15.5-5.4c6.6-1.1,8.2-3.6,8.2-12.8v-5.9h4v6.9c0,6.6,0.1,7,1.8,8.8
c1.5,1.6,3.9,1.7,9.5,3.1c20.4,5,35.3,20.5,40.7,40.1c0.8,2.9,1.8,6.4,2.2,7.8c1.2,4.3,3.8,5.6,11.7,5.6h6.5v4h-6.6
c-7,0-9.9,0.8-10.8,3.1c-0.3,0.7-1.3,4.6-2.1,8c-5.4,21.4-21.3,36.7-42.1,41.5c-9.9,2.3-10.7,3.2-10.7,12.8v6.3h-3.8L823.6,602.7
L823.6,602.7z"/>
<path id="path1504" class="st7" d="M752.2,526.5c1.8-15.3,8.9-31.2,20.4-43c11.9-12.2,27.5-19.2,45.7-21.6l2.8-0.4v6.2
c0,7.9-0.1,8.9-6.8,10.2c-22.3,4.6-41.8,22.1-46.1,44.2c-1.8,9.1-2.4,9.1-11.1,9.1h-5.4L752.2,526.5L752.2,526.5z"/>
<path id="path1506" class="st7" d="M811.2,608.2c-32.2-6.9-55.7-32.9-59.4-65.6l-0.2-2.1h6.4c6.9,0,6.8,0,7.8,1
c1.3,1.4,1.5,3.8,2.4,7.9c4.8,21.2,24.2,39.1,46.6,44c6.9,1.5,6.4,2.7,6.4,10.3v5.9l-2.1,0C817.8,609.5,814.3,608.9,811.2,608.2
L811.2,608.2L811.2,608.2z"/>
<path id="path1508" class="st7" d="M830,603c-0.3-7.3,1-8.2,5.3-9c4.8-0.8,11.1-2.9,16.3-5.5c15.8-7.7,27.1-22,31.6-39.9
c1-4,1.6-7.2,2.2-7.7c0.7-0.5,3.9-0.5,7.8-0.5h6.2l-0.4,3.8c-3.8,33.2-31.8,61.3-64.8,65l-4,0.5L830,603L830,603z"/>
<path id="path1510" class="st7" d="M885.4,529.8c-1.2-1.1-1.3-3-2.1-6.7c-4.4-20.5-19-36.6-39.3-43.5c-2.4-0.8-5.8-1.7-7.5-2
c-1.9-0.3-3.9-0.6-4.8-1.5c-1.4-1.4-1.3-2.6-1.3-8.2v-6.2l2.5,0.3c19,2.5,32.7,9.2,45.4,22.1c10.4,10.5,17.4,23.8,20.2,38.1
c0.6,2.9,1,6.1,1,7.1v1.9h-5.9C888.3,531.3,886.8,531.1,885.4,529.8L885.4,529.8L885.4,529.8z"/>
<path id="path1512" class="st4" d="M720.3,131c-0.5-0.6-0.6-1.7-0.6-7.6v-6.9l-1.1-1.2l-1.1-1.2l-7.1-0.1
c-4.1-0.1-6.5,0.5-7.6-0.2c-1.3-0.8-0.9-3.1-0.9-7.6s0.1-6.3,0.5-6.8c0.5-0.7,1.1-0.7,7.3-0.8c3.7-0.1,7.1-0.2,7.6-0.3
c0.5-0.1,1.2-0.7,1.6-1.4c0.7-1.1,0.7-1.9,0.7-7.8c0-3.8,0.2-6.8,0.4-7.3c0.4-0.7,0.9-0.7,7.3-0.7s7,0.1,7.3,0.7
c0.2,0.4,0.4,3.5,0.4,7.3c0,5.8,0.1,6.7,0.7,7.8c0.4,0.7,1.1,1.3,1.6,1.4s3.9,0.2,7.6,0.3c6.2,0.1,6.8,0.2,7.3,0.8
c0.4,0.6,0.5,2.2,0.5,6.8s0.7,7.2-0.9,7.9c-1.2,0.5-3.8-0.1-7.6-0.1l-7.1,0.1l-1.1,1.2l-1.1,1.2v6.9c0,6.2-0.1,7-0.7,7.6
c-0.6,0.5-1.7,0.6-7.1,0.6C721.7,131.7,720.9,131.6,720.3,131L720.3,131L720.3,131z"/>
<path id="path1514" class="st7" d="M766.6,726.4v-5.3h10.5v10.5h-10.5V726.4L766.6,726.4z"/>
<path id="path1518" class="st7" d="M766,761.6c-14.8-2.3-27.2-12.3-32.4-26.2c-7.5-20,2.3-43,22-51.4c6.1-2.6,8-3.5,15.7-3.5
c6.5,0,7.9,0.6,11.3,1.6c14.4,4.4,24.6,14.9,28.6,29.4c0.8,2.9,1.1,5.2,1.1,10.7c0,5.5-0.3,6.2-1.1,9c-4.8,17.7-20,30-38.1,30.5
C770.5,761.9,767.4,761.8,766,761.6L766,761.6L766,761.6z M778.1,749.7c11.9-2.6,21.3-13,22.5-25.1c2.2-21.5-18.1-37.7-38.5-30.7
c-4.2,1.4-7,3.3-10.8,7.1c-2.8,2.8-3.8,4.1-5.3,7.2c-2.6,5.1-3.3,8.7-3,14.6c0.3,6.8,2.2,11.7,6.3,16.9
C755.9,748,767.4,752,778.1,749.7L778.1,749.7L778.1,749.7z"/>
<path id="path1520" class="st9" d="M648.2,714.3c-1.3-0.8-2.3-0.9-2.5-5.3c-0.2-4.3,0.1-13.5,0.1-32.6c0-32.8-0.5-35.2,0.6-36.3
c0.1-0.1,0.1-0.2,0.2-0.2c1.1-1.5,2.7-2.1,4.9-2.1h1.9V715h-2C650.1,715,648.9,714.8,648.2,714.3L648.2,714.3L648.2,714.3z"/>
<path id="path1522" class="st9" d="M649.6,312.1c-0.5-0.1-1.3-0.4-1.8-0.8c-0.8-0.5-1.5-0.8-1.8-2.7c-0.5-3.2-0.2-11.4-0.2-35.1
c0-19.3-0.5-28.4-0.4-32.5c0.1-2.7,0.8-3.2,1-3.6c0.9-1.6,2.4-2.3,4.8-2.3h2.1v77.3l-1.4,0C651.2,312.3,650.1,312.2,649.6,312.1
L649.6,312.1L649.6,312.1z"/>
<path id="path1524" class="st8" d="M978,126.4c-10.1-20.6-21.9-37.2-38.2-53.9c-34.8-35.7-78.7-57-129.6-63.1
c-5.1-0.6-13.1-0.8-40.9-1l-34.6-0.2V6.9c0-1,0-3,1.4-3.7l2.1-1.1l30.4,0.2c16.9,0.1,33.6,0.2,37,0.5
c50.8,3.9,97.1,24.6,133.3,59.5c17.6,16.9,31.5,35.7,42.8,57.9c4.3,8.4,4.9,10.1,4.2,11.8c-0.4,1.1-2.7,4.2-3.2,4.1
C982.6,136.1,980.5,131.5,978,126.4L978,126.4L978,126.4z"/>
<path id="path1530" class="st4" d="M646.8,903.8c-0.6-0.5-1.7-1.2-1.6-5c0.3-6.7-0.2-27.2-0.1-79.1l0.3-82.6l2.6-6.5l7.9-15
l0.1-39.4l-0.4-39.6l-7.1-13.2l-2.9-7.7l-0.4-7.3l0.1-48l-0.1-22.1l0.1-34.1l0.1-10.5V483l-0.1-8.6l0-25.6v-19.4l-0.2-27.5
l0.1-21.6l0-11l0.1-19.6l0-10.2l0.5-5.7l1.9-5.8l3-5.5l5.3-9.8v-39.5l-0.4-39.6l-5.3-9.8l-2.7-5.3l-1.8-5.1l-0.7-7.6l0.2-13.3
l-0.1-12.2v-11.1l-0.1-10.9l0-9.9V142l0-2.9l0.1-17.4l0-21.5V89.4l-0.1-8.4l0-5.1l-0.2-5.9l2.2-3.1l3.2-1.1l3.2,0.1
c1.4,0,2.7,0.1,3.9,0.2c0.9,0.1,1.8,0.1,2.6,0.4c1,0.4,1.7,1,2,1.3c0.2,0.2,1.2,1.7,1.3,4.8c0.1,2.1,0,4.7,0,8.3
c0,4.1,0.1,9.7,0,16.4c-0.1,10,0,23.1-0.1,40.4c0,13.2,0.2,28.7,0.1,47c-0.2,62.8-0.2,158-0.2,301l0,416.2l-1.3,1.8
C660.4,906.2,649.5,906.5,646.8,903.8L646.8,903.8L646.8,903.8z"/>
<path id="path1532" class="st4" d="M665.8,465.1V72.7h8.6v784.7h-8.6V465.1L665.8,465.1z"/>
<ellipse id="path3879" class="st14" cx="771.6" cy="721.1" rx="40.3" ry="40.3"/>
<ellipse id="path3881" class="st15" cx="771.7" cy="721.4" rx="34.1" ry="34.5"/>
<path id="path96" d="M771.7,694.3l-26.8,26.4h7.7v22.5h38.5v-22.5h7.2L771.7,694.3L771.7,694.3z M779.5,735.2h-15.2v-14.5h15.2
V735.2z"/>
</g>
</g>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="1000"
height="1000"
viewBox="0 0 264.58333 264.58333"
version="1.1"
id="svg1"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<style
type="text/css"
id="style1">
.button-letter{fill:#ffffff;}
.button{fill:#44484c;}
</style>
<g
id="layer1">
<path
id="path3828"
d="m 93.197715,62.688345 h -2.620729 v 21.545122 h 2.56557 c 0.89606,0 1.351744,-0.624998 1.351744,-1.35174 V 63.819394 c 0,-0.689696 -0.504663,-1.131049 -1.296585,-1.131049 z"
style="fill:#00bbdb;fill-opacity:1;stroke:#000000;stroke-width:0.624212px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
style="fill:#00bbdb;fill-opacity:1;stroke:#000000;stroke-width:0.624212px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 93.197715,189.88155 h -2.620729 v -21.5451 h 2.56557 c 0.89606,0 1.351744,0.62498 1.351744,1.35171 v 19.06237 c 0,0.68969 -0.504663,1.13102 -1.296585,1.13102 z"
id="path3830" />
<path
id="path3826"
d="M 93.528763,17.942907 H 84.783801 V 227.42817 h 4.091826 v 8.69996 c 0,0.97215 0.816577,2.6123 1.50821,2.6123 h 3.046593 c 0.558879,0 1.062338,-0.43161 1.062338,-1.0623 v -40.72588 c 0,-5.38012 -3.364052,-5.44616 -3.364052,-7.14492 v -21.6003 c 0,-1.53274 3.282823,-1.90985 3.282823,-6.28972 V 90.412872 c 0,-4.341563 -3.282823,-4.633568 -3.282823,-6.289744 l 0.07809,-21.241668 c 0,-1.463087 3.282799,-2.59006 3.282799,-6.482858 l -0.0018,-37.331542 c 0.0049,-0.612796 -0.296036,-1.124153 -0.95865,-1.124153 z"
style="fill:#44484c;fill-opacity:1;stroke:#000000;stroke-width:0.624212px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="LeftShoulder"
class="button"
d="M 71.141642,4.5840631 V 1.9701729 c 0,-0.7845694 -0.801336,-1.65806689 -1.658066,-1.65806689 H 53.097985 c -25.276611,0 -47.9020118,20.38799499 -51.7794075,34.85862199 -0.1769423,0.660145 -0.2403108,1.986235 0.910356,1.986235 h 2.9103949 z"
style="fill-opacity:1;stroke:#000000;stroke-width:0.624212px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
id="path3925"
d="M 85.524929,5.5446231 V 263.1496 c 0,0.63887 -0.315806,1.12163 -1.121649,1.12163 H 53.416952 c -32.259013,0 -53.10484599,-28.52508 -53.10484599,-53.10484 V 56.398494 C 0.31210601,20.26586 35.745182,3.5158114 53.194784,3.5158114 h 30.455738 c 1.551854,0 1.874407,0.8539058 1.874407,2.0288117 z"
style="display:inline;fill:#00bbdb;fill-opacity:1;stroke:#000000;stroke-width:0.624212px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<circle
id="path3871"
style="fill:#44484c;fill-opacity:1;stroke:#000000;stroke-width:0.624212;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="45.383232"
cy="125.1807"
r="9.4570808" />
<circle
style="fill:#44484c;fill-opacity:1;stroke:#000000;stroke-width:0.624212;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path3873"
cx="45.383232"
cy="164.27483"
r="9.4570808" />
<circle
style="fill:#44484c;fill-opacity:1;stroke:#000000;stroke-width:0.624212;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path3875"
transform="rotate(90)"
cx="144.72777"
cy="-25.836153"
r="9.4570808" />
<circle
transform="rotate(90)"
id="path3877"
style="fill:#44484c;fill-opacity:1;stroke:#000000;stroke-width:0.624212;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="144.72777"
cy="-64.93026"
r="9.4570808" />
<rect
style="fill-opacity:1;stroke:#000000;stroke-width:0.624212;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="Back"
class="button"
width="13.84973"
height="3.9793589"
x="64.770714"
y="27.445877" />
<circle
id="path3907"
style="fill:#44484c;fill-opacity:1;stroke:#000000;stroke-width:0.624212;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="45.459122"
cy="71.1408"
r="20.013832" />
<path
id="path3913"
d="m 45.283504,90.058853 c -0.243093,-0.02724 -0.452358,-0.208017 -0.528892,-0.456894 -0.01416,-0.04954 -0.01651,-0.20608 -0.01888,-2.021978 l -0.0023,-1.968328 0.14415,0.0081 c 0.07928,0.0039 0.358107,0.0081 0.619698,0.0081 h 0.475597 l -0.0023,1.970175 -0.0023,1.970162 -0.02123,0.06335 c -0.06511,0.190578 -0.215869,0.340259 -0.406589,0.404173 -0.06227,0.02072 -0.180481,0.03175 -0.254751,0.02338 v 0 z"
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.0195065;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path3913-8"
d="m 45.434622,52.223145 c 0.247163,0.02887 0.459932,0.220525 0.537747,0.484365 0.0144,0.05251 0.01677,0.218471 0.0192,2.143553 l 0.0023,2.086678 -0.146563,-0.0084 c -0.0806,-0.0041 -0.364103,-0.0084 -0.630075,-0.0084 h -0.483559 l 0.0023,-2.088635 0.0023,-2.088621 0.02158,-0.06716 c 0.0662,-0.202038 0.219482,-0.360718 0.413396,-0.428476 0.0633,-0.02196 0.183503,-0.03366 0.259017,-0.02479 v 0 z"
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.0202518;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path3915"
d="m 59.806505,71.322008 c 0,-0.221406 -0.0049,-0.500259 -0.007,-0.619683 l -0.007,-0.217117 2.017096,0.0027 2.017072,0.0026 0.07149,0.02617 c 0.269118,0.0983 0.436388,0.346437 0.418197,0.620415 -0.0094,0.151847 -0.0696,0.285696 -0.175291,0.393668 -0.0828,0.08475 -0.1609,0.129639 -0.304646,0.175783 -0.03113,0.0093 -0.444127,0.01328 -2.033563,0.01527 l -1.994565,0.0027 v -0.402555 0 z"
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.0195065;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
id="path3917"
d="m 26.928027,71.713911 c -0.13424,-0.01766 -0.265319,-0.0854 -0.360891,-0.186526 -0.332557,-0.351564 -0.164911,-0.91291 0.307243,-1.028164 0.04412,-0.01067 0.430158,-0.01329 2.153412,-0.01342 l 2.099786,-3.59e-4 -0.007,0.207261 c -0.0049,0.113964 -0.007,0.392698 -0.007,0.619339 v 0.412078 l -2.062819,-0.0012 c -1.134577,-6.38e-4 -2.08917,-0.0054 -2.12135,-0.0093 z"
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.0195065;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<circle
style="fill-opacity:1;stroke:#000000;stroke-width:0.624212;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="LeftStick"
class="button"
cx="45.459122"
cy="71.279633"
r="14.034906" />
<rect
style="fill:#44484c;fill-opacity:1;stroke:#000000;stroke-width:0.624212;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect3919"
width="16.073488"
height="15.683355"
x="51.740257"
y="186.54222"
ry="2.1067193" />
<circle
style="fill:#000000;stroke:#000000;stroke-width:0.624212;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path3921"
cx="59.718506"
cy="194.52045"
r="5.5203857" />
<path
style="stroke:#000000;stroke-width:0.904694;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="DpadRight"
d="m 134.97306,862.5577 0,-11.46794 9.93153,5.73397 z"
transform="matrix(0.68997127,0,0,0.68997127,-30.716968,-446.43677)" />
<path
transform="matrix(-0.68997127,0,0,0.68997127,121.58263,-446.43677)"
d="m 134.97306,862.5577 0,-11.46794 9.93153,5.73397 z"
id="DpadLeft"
style="stroke:#000000;stroke-width:0.904694;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
transform="matrix(0,-0.68997127,0.68997127,0,-545.81256,221.00462)"
d="m 134.97306,862.5577 0,-11.46794 9.93153,5.73397 z"
id="DpadUp"
style="stroke:#000000;stroke-width:0.904694;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
style="stroke:#000000;stroke-width:0.904694;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="DpadDown"
d="m 134.97306,862.5577 0,-11.46794 9.93153,5.73397 z"
transform="matrix(0,0.68997127,0.68997127,0,-545.81256,68.705012)" />
<path
style="fill:#ff5f53;fill-opacity:1;stroke:#000000;stroke-width:0.623304px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 171.63364,62.597166 h 2.61697 v 21.51379 h -2.56186 c -0.89476,0 -1.34976,-0.6241 -1.34976,-1.34978 v -19.0346 c 0,-0.68871 0.50393,-1.12941 1.29465,-1.12941 z"
id="path3822" />
<path
id="path3824"
d="m 171.63364,189.60538 h 2.61697 v -21.5138 h -2.56186 c -0.89476,0 -1.34976,0.6241 -1.34976,1.34978 v 19.0346 c 0,0.68872 0.50393,1.12942 1.29465,1.12942 z"
style="fill:#ff5f53;fill-opacity:1;stroke:#000000;stroke-width:0.623304px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
<path
style="fill:#44484c;fill-opacity:1;stroke:#000000;stroke-width:0.623304px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 171.3031,17.916809 h 8.73227 V 227.09738 h -4.0859 v 8.68732 c 0,0.97073 -0.81539,2.6085 -1.50602,2.6085 h -3.04216 c -0.55806,0 -1.06074,-0.43099 -1.06074,-1.06076 V 196.6658 c 0,-5.3723 3.35911,-5.43824 3.35911,-7.13454 v -21.56887 c 0,-1.53052 -3.27803,-1.90709 -3.27803,-6.28061 V 90.281366 c 0,-4.33524 3.27803,-4.62682 3.27803,-6.28059 l -0.078,-21.21078 c 0,-1.46096 -3.27802,-2.58628 -3.27802,-6.47342 v -37.27725 c -0.007,-0.611905 0.29555,-1.122517 0.9572,-1.122517 z"
id="path3820" />
<path
style="fill-opacity:1;stroke:#000000;stroke-width:0.623304px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 193.42147,4.577397 V 1.967308 c 0,-0.783429 0.80019,-1.655656 1.65568,-1.655656 h 16.36178 c 25.2398,0 47.83231,20.35834 51.70409,34.807924 0.17669,0.65918 0.23999,1.98334 -0.90903,1.98334 h -2.90616 z"
class="button"
id="RightShoulder" />
<path
style="fill:#ff5f53;fill-opacity:1;stroke:#000000;stroke-width:0.623304px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 179.18287,5.921399 V 263.15167 c 0,0.63795 0.31533,1.12001 1.12002,1.12001 h 30.94124 c 32.21211,0 53.02763,-28.48359 53.02763,-53.0276 V 56.701296 c 0,-36.080072 -35.38154,-52.805757 -52.80579,-52.805757 h -30.41144 c -1.5496,0 -1.87166,0.852662 -1.87166,2.02586 z"
id="path3923" />
<path
style="fill-opacity:1;stroke:#000000;stroke-width:0.623304px;stroke-linecap:round;stroke-linejoin:round;stroke-opacity:1"
d="m 186.21364,27.4046 h 4.74192 v -4.6829 h 3.93517 v 4.702571 h 4.66326 v 3.935212 h -4.64356 v 4.859983 h -3.99421 v -4.859983 h -4.68296 z"
class="button"
id="Start" />
<circle
style="fill:#44484c;fill-opacity:1;stroke:#000000;stroke-width:0.623304;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path3095"
cx="218.91861"
cy="49.868065"
r="9.443327" />
<circle
id="path3865"
style="fill:#44484c;fill-opacity:1;stroke:#000000;stroke-width:0.623304;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="218.91861"
cy="88.905327"
r="9.443327" />
<circle
transform="rotate(90)"
id="path3867"
style="fill:#44484c;fill-opacity:1;stroke:#000000;stroke-width:0.623304;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="69.386696"
cy="-199.39992"
r="9.443327" />
<circle
style="fill:#44484c;fill-opacity:1;stroke:#000000;stroke-width:0.623304;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path3869"
transform="rotate(90)"
cx="69.386696"
cy="-238.43718"
r="9.443327" />
<circle
id="path3879"
style="fill:#999595;fill-opacity:1;stroke:#000000;stroke-width:0.623304;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="204.73285"
cy="190.58415"
r="10.489907" />
<circle
style="fill:#3a3d40;fill-opacity:1;stroke:#000000;stroke-width:0.623304;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path3881"
cx="204.73282"
cy="190.67955"
r="7.834487" />
<path
style="fill:#000000;stroke:#000000;stroke-width:0.623304;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 199.22201,190.26217 h 1.54823 v 4.87267 c 2.59543,0 5.4958,0 7.85494,0 v -4.87267 h 1.54819 c -1.7057,-1.58618 -4.00126,-3.75231 -5.47568,-5.10089 -1.78447,1.64041 -3.66522,3.41042 -5.47568,5.10089 z m 3.65045,0.0815 c 1.20766,0 2.55586,0 3.65045,0 v 3.1127 c -1.2064,0 -2.55429,0 -3.65045,0 z"
id="path3883" />
<circle
style="fill:#44484c;fill-opacity:1;stroke:#000000;stroke-width:0.623304;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path3888"
cx="219.03548"
cy="141.86038"
r="19.984724" />
<path
style="fill:#000000;stroke:#000000;stroke-width:0.0194783;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 218.31129,125.58354 c 1.4e-4,-1.33699 0.007,-2.12691 0.009,-2.16939 0.0188,-0.12886 0.082,-0.25202 0.18022,-0.35026 0.29619,-0.29619 0.79996,-0.20825 0.98258,0.1715 0.0686,0.14229 0.0636,-0.0302 0.0638,2.31736 l 1.4e-4,2.12043 -0.43719,-5.2e-4 c -0.24048,-2.8e-4 -0.51879,0.002 -0.61844,0.007 l -0.18116,0.007 1.9e-4,-2.1026 z"
id="path3899" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.0194783;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 218.86005,160.75093 c -0.24272,-0.0271 -0.4517,-0.20778 -0.52813,-0.45624 -0.0141,-0.0495 -0.0165,-0.2059 -0.0188,-2.01902 l -0.007,-1.96549 0.14394,0.007 c 0.0791,0.005 0.35759,0.007 0.61882,0.007 h 0.47488 v 1.9673 1.96731 l -0.0212,0.0634 c -0.065,0.19034 -0.21532,0.33975 -0.40595,0.40359 -0.0624,0.0212 -0.18045,0.0318 -0.25438,0.0236 v 0 z"
id="path3901" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.0194783;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 233.36192,142.04134 c 0,-0.22121 -0.007,-0.49955 -0.007,-0.6188 l -0.007,-0.21673 2.01414,0.002 2.01421,0.002 0.0714,0.0262 c 0.2687,0.0982 0.43575,0.34595 0.41756,0.61953 -0.009,0.15171 -0.0695,0.28529 -0.17503,0.39309 -0.0827,0.0846 -0.16067,0.12957 -0.30418,0.1755 -0.0311,0.009 -0.44348,0.0141 -2.03061,0.0165 l -1.99164,0.002 v -0.40197 0 z"
id="path3903" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.0194783;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 200.53132,142.43266 c -0.13405,-0.0188 -0.26496,-0.0853 -0.36039,-0.18634 -0.3321,-0.35104 -0.16467,-0.91158 0.30679,-1.02666 0.0443,-0.0118 0.42951,-0.0141 2.15028,-0.0141 l 2.09673,-3.6e-4 -0.007,0.20684 c -0.007,0.11379 -0.007,0.39212 -0.007,0.61845 v 0.41148 l -2.05984,-0.001 c -1.13291,-6.4e-4 -2.08613,-0.005 -2.11824,-0.009 z"
id="path3905" />
<circle
id="RightStick"
class="button"
style="fill-opacity:1;stroke:#000000;stroke-width:0.623304;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
cx="219.03548"
cy="141.99901"
r="14.014492" />
<path
style="stroke:none;stroke-width:0.623304"
d="m 214.35736,54.921716 c 0,-0.007 0.76648,-1.18123 1.70331,-2.61159 l 1.70329,-2.60093 -1.50957,-2.33178 c -0.83025,-1.28245 -1.52861,-2.35884 -1.55183,-2.39188 l -0.0424,-0.0598 h 1.16815 1.16812 l 1.00159,1.60601 c 0.55088,0.88334 1.01121,1.60283 1.02292,1.5989 0.0118,-0.007 0.46006,-0.72416 0.99629,-1.60052 l 0.97485,-1.59348 1.15081,-0.007 c 0.63291,-0.007 1.15073,0 1.15073,0.007 0,0.007 -0.69378,1.09458 -1.54187,2.41861 -0.88264,1.37819 -1.53793,2.42229 -1.5327,2.44236 0.007,0.0193 0.76773,1.17686 1.69483,2.5725 0.9271,1.39558 1.68567,2.54309 1.68567,2.55007 0,0.007 -0.53828,0.0118 -1.19625,0.0118 h -1.19625 l -1.11192,-1.73772 c -0.89798,-1.40349 -1.11707,-1.73229 -1.13877,-1.70891 -0.0141,0.0156 -0.52084,0.79826 -1.12449,1.73865 l -1.09766,1.70972 h -1.18846 c -0.65366,0 -1.1885,-0.007 -1.1885,-0.0106 z"
id="X"
class="button-letter" />
<path
style="stroke:none;stroke-width:0.623304"
d="m 233.66024,74.293476 c 0.0375,-0.0993 3.7808,-9.70953 3.82834,-9.82896 l 0.0542,-0.1365 h 1.0542 1.0542 l 1.99265,4.97454 c 1.09596,2.736 1.99762,4.98688 2.00375,5.00202 0.009,0.0219 -0.20896,0.0262 -1.07672,0.0212 l -1.08791,-0.006 -0.43799,-1.14115 -0.43799,-1.14114 -2.00924,0.006 -2.00921,0.006 -0.41427,1.14127 -0.41424,1.14121 h -1.05719 c -0.99785,0 -1.05644,-0.006 -1.0428,-0.038 z m 6.29682,-3.94253 c -0.007,-0.0151 -0.31794,-0.85573 -0.69393,-1.86829 -0.37591,-1.01256 -0.69006,-1.83327 -0.69802,-1.82386 -0.0118,0.0125 -0.81417,2.21149 -1.34922,3.69215 -0.007,0.0212 0.27556,0.0268 1.37084,0.0268 1.09559,0 1.37862,-0.006 1.37033,-0.0268 z"
id="A"
class="button-letter" />
<path
style="stroke:none;stroke-width:0.623304"
d="m 215.2136,89.120286 v -4.99274 l 2.4681,0.009 c 2.46075,0.0118 2.72639,0.0188 3.21986,0.0935 0.74879,0.11379 1.36342,0.49628 1.76906,1.10127 0.6167,0.91968 0.51717,2.11947 -0.24264,2.92491 -0.17716,0.18776 -0.3468,0.31926 -0.58323,0.45178 l -0.18352,0.10294 0.14866,0.0492 c 0.36246,0.11944 0.74591,0.33919 1.00328,0.57481 0.30204,0.27636 0.56214,0.71499 0.67522,1.13859 0.0518,0.19294 0.0573,0.25487 0.0591,0.61757 0,0.31452 -0.007,0.44272 -0.0379,0.58334 -0.1258,0.57277 -0.40347,1.10525 -0.77185,1.48074 -0.47147,0.48049 -1.04977,0.72422 -1.87578,0.79054 -0.41144,0.033 -2.41554,0.0643 -4.16112,0.065 h -1.48738 v -4.99274 z m 4.97634,3.2926 c 0.46106,-0.0379 0.70688,-0.13169 0.95047,-0.36149 0.25189,-0.23761 0.35622,-0.51213 0.35542,-0.9349 0,-0.65174 -0.35417,-1.10836 -0.9675,-1.25173 -0.35523,-0.0829 -0.56372,-0.0942 -1.95312,-0.10437 l -1.35664,-0.009 v 1.34229 1.34236 h 1.35664 c 0.74608,-1.9e-4 1.47275,-0.009 1.61473,-0.0212 v 0 z m -0.80295,-4.31363 c 0.72759,-0.0212 0.87325,-0.0417 1.13555,-0.16608 0.27438,-0.13004 0.46767,-0.34631 0.55898,-0.62519 0.0624,-0.19058 0.0624,-0.57978 -6.4e-4,-0.7735 -0.11755,-0.36345 -0.3875,-0.5975 -0.80238,-0.69581 -0.12934,-0.0306 -0.3557,-0.0368 -1.60531,-0.045 l -1.45466,-0.009 v 1.16751 1.16758 h 0.75724 c 0.41656,0 1.05152,-0.009 1.41111,-0.0188 v 0 z"
id="B"
class="button-letter" />
<path
style="stroke:none;stroke-width:0.623304"
d="m 198.44799,72.424996 v -2.1001 l -1.81569,-2.87051 c -0.99867,-1.57871 -1.81575,-2.8745 -1.81575,-2.87942 0,-0.006 0.52108,-0.009 1.15792,-0.009 l 1.15797,2.5e-4 1.17418,1.97688 c 0.87332,1.47019 1.18042,1.97033 1.19825,1.95138 0.0141,-0.0137 0.53524,-0.90373 1.16002,-1.97706 l 1.13592,-1.95163 h 1.14209 1.14202 l -0.0311,0.0492 c -0.0165,0.0268 -0.8403,1.32677 -1.82909,2.88865 l -1.79774,2.83972 v 2.09094 2.09094 h -0.98943 -0.98939 v -2.10017 z"
id="Y"
class="button-letter" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -1,13 +0,0 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style Selector="MenuItem.withCheckbox Viewbox#PART_IconPresenter">
<Setter Property="MaxHeight" Value="36" />
<Setter Property="MinHeight" Value="36" />
<Setter Property="MaxWidth" Value="36" />
<Setter Property="MinWidth" Value="36" />
</Style>
<Style Selector="MenuItem.withCheckbox ContentPresenter#PART_HeaderPresenter">
<Setter Property="Padding" Value="-10,0,0,0" />
</Style>
</Styles>

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
using Avalonia.Controls.Notifications;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using Gommon;
using LibHac;
using LibHac.Account;
using LibHac.Common;
@@ -15,7 +15,6 @@ using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
@@ -296,150 +295,22 @@ namespace Ryujinx.Ava.Common
};
extractorThread.Start();
}
public static void ExtractAoc(string destination, string updateFilePath, string updateName)
{
var cancellationToken = new CancellationTokenSource();
UpdateWaitWindow waitingDialog = new(
RyujinxApp.FormatTitle(LocaleKeys.DialogNcaExtractionTitle),
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogNcaExtractionMessage, NcaSectionType.Data, Path.GetFileName(updateFilePath)),
cancellationToken);
Thread extractorThread = new(() =>
{
Dispatcher.UIThread.Post(waitingDialog.Show);
using FileStream file = new(updateFilePath, FileMode.Open, FileAccess.Read);
Nca publicDataNca = null;
string extension = Path.GetExtension(updateFilePath).ToLower();
if (extension is ".nsp")
{
var pfsTemp = new PartitionFileSystem();
pfsTemp.Initialize(file.AsStorage()).ThrowIfFailure();
IFileSystem pfs = pfsTemp;
foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca"))
{
using var ncaFile = new UniqueRef<IFile>();
pfs.OpenFile(ref ncaFile.Ref, fileEntry.FullPath.ToU8Span(), OpenMode.Read).ThrowIfFailure();
Nca nca = new(_virtualFileSystem.KeySet, ncaFile.Get.AsStorage());
if (nca.Header.ContentType is NcaContentType.PublicData && nca.SectionExists(NcaSectionType.Data))
{
publicDataNca = nca;
}
}
}
if (publicDataNca is null)
{
Logger.Error?.Print(LogClass.Application, "Extraction failure. The NCA was not present in the selected file");
Dispatcher.UIThread.InvokeAsync(async () =>
{
waitingDialog.Close();
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionMainNcaNotFoundErrorMessage]);
});
return;
}
IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks
? IntegrityCheckLevel.ErrorOnInvalid
: IntegrityCheckLevel.None;
int index = Nca.GetSectionIndexFromType(NcaSectionType.Data, publicDataNca.Header.ContentType);
try
{
IFileSystem ncaFileSystem = publicDataNca.OpenFileSystem(index, IntegrityCheckLevel.ErrorOnInvalid);
FileSystemClient fsClient = _horizonClient.Fs;
string source = DateTime.Now.ToFileTime().ToString()[10..];
string output = DateTime.Now.ToFileTime().ToString()[10..];
using var uniqueSourceFs = new UniqueRef<IFileSystem>(ncaFileSystem);
using var uniqueOutputFs = new UniqueRef<IFileSystem>(new LocalFileSystem(destination));
fsClient.Register(source.ToU8Span(), ref uniqueSourceFs.Ref);
fsClient.Register(output.ToU8Span(), ref uniqueOutputFs.Ref);
(Result? resultCode, bool canceled) = CopyDirectory(fsClient, $"{source}:/", $"{output}:/", cancellationToken.Token);
if (!canceled)
{
if (resultCode.Value.IsFailure())
{
Logger.Error?.Print(LogClass.Application, $"LibHac returned error code: {resultCode.Value.ErrorCode}");
Dispatcher.UIThread.InvokeAsync(async () =>
{
waitingDialog.Close();
await ContentDialogHelper.CreateErrorDialog(LocaleManager.Instance[LocaleKeys.DialogNcaExtractionCheckLogErrorMessage]);
});
}
else if (resultCode.Value.IsSuccess())
{
Dispatcher.UIThread.Post(waitingDialog.Close);
NotificationHelper.ShowInformation(
RyujinxApp.FormatTitle(LocaleKeys.DialogNcaExtractionTitle),
$"{updateName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}");
}
}
fsClient.Unmount(source.ToU8Span());
fsClient.Unmount(output.ToU8Span());
}
catch (ArgumentException ex)
{
Logger.Error?.Print(LogClass.Application, $"{ex.Message}");
Dispatcher.UIThread.InvokeAsync(async () =>
{
waitingDialog.Close();
await ContentDialogHelper.CreateErrorDialog(ex.Message);
});
}
})
{
Name = "GUI.AocExtractorThread",
IsBackground = true,
};
extractorThread.Start();
}
public static async Task ExtractAoc(IStorageProvider storageProvider, string updateFilePath, string updateName)
{
Optional<IStorageFolder> result = await storageProvider.OpenSingleFolderPickerAsync(new FolderPickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle]
});
if (!result.HasValue) return;
ExtractAoc(result.Value.Path.LocalPath, updateFilePath, updateName);
}
public static async Task ExtractSection(IStorageProvider storageProvider, NcaSectionType ncaSectionType, string titleFilePath, string titleName, int programIndex = 0)
{
Optional<IStorageFolder> result = await storageProvider.OpenSingleFolderPickerAsync(new FolderPickerOpenOptions
var result = await storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle]
Title = LocaleManager.Instance[LocaleKeys.FolderDialogExtractTitle],
AllowMultiple = false,
});
if (!result.HasValue) return;
if (result.Count == 0)
{
return;
}
ExtractSection(result.Value.Path.LocalPath, ncaSectionType, titleFilePath, titleName, programIndex);
ExtractSection(result[0].Path.LocalPath, ncaSectionType, titleFilePath, titleName, programIndex);
}
public static (Result? result, bool canceled) CopyDirectory(FileSystemClient fs, string sourcePath, string destPath, CancellationToken token)

View File

@@ -6,7 +6,7 @@ namespace Ryujinx.Ava.Common.Models
public bool IsBundled { get; } = System.IO.Path.GetExtension(ContainerPath)?.ToLower() == ".xci";
public string FileName => System.IO.Path.GetFileName(ContainerPath);
public string TitleIdStr => TitleId.ToString("x16").ToUpper();
public string TitleIdStr => TitleId.ToString("x16");
public ulong TitleIdBase => TitleId & ~0x1FFFUL;
}
}

View File

@@ -1,6 +1,6 @@
using DiscordRPC;
using LibHac.Tools.FsSystem;
using Ryujinx.Audio.Backends.SDL2;
using Ryujinx.Audio.Backends.SDL3;
using Ryujinx.Ava;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Common.Configuration;
@@ -157,7 +157,7 @@ namespace Ryujinx.Headless
config = new StandardControllerInputConfig
{
Version = InputConfig.CurrentVersion,
Backend = InputBackendType.GamepadSDL2,
Backend = InputBackendType.GamepadSDL3,
Id = null,
ControllerType = ControllerType.JoyconPair,
DeadzoneLeft = 0.1f,
@@ -335,7 +335,7 @@ namespace Ryujinx.Headless
_accountManager,
_userChannelPersistence,
renderer,
new SDL2HardwareDeviceDriver(),
new SDL3HardwareDeviceDriver(),
options.DramSize,
window,
options.SystemLanguage,

View File

@@ -21,8 +21,8 @@ using Ryujinx.HLE.HOS;
using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.Input;
using Ryujinx.Input.HLE;
using Ryujinx.Input.SDL2;
using Ryujinx.SDL2.Common;
using Ryujinx.Input.SDL3;
using Ryujinx.SDL3.Common;
using System;
using System.Collections.Generic;
using System.IO;
@@ -59,7 +59,7 @@ namespace Ryujinx.Headless
AutoResetEvent invoked = new(false);
// MacOS must perform SDL polls from the main thread.
SDL2Driver.MainThreadDispatcher = action =>
SDL3Driver.MainThreadDispatcher = action =>
{
invoked.Reset();
@@ -180,7 +180,7 @@ namespace Ryujinx.Headless
_accountManager = new AccountManager(_libHacHorizonManager.RyujinxClient, option.UserProfile);
_userChannelPersistence = new UserChannelPersistence();
_inputManager = new InputManager(new SDL2KeyboardDriver(), new SDL2GamepadDriver());
_inputManager = new InputManager(new SDL3KeyboardDriver(), new SDL3GamepadDriver());
GraphicsConfig.EnableShaderCache = !option.DisableShaderCache;

View File

@@ -149,7 +149,7 @@ namespace Ryujinx.Headless
IgnoreMissingServices = configurationState.System.IgnoreMissingServices;
if (NeedsOverride(nameof(IgnoreControllerApplet)))
IgnoreControllerApplet = configurationState.System.IgnoreApplet;
IgnoreControllerApplet = configurationState.IgnoreApplet;
return;

View File

@@ -1,9 +1,9 @@
using Ryujinx.Common.Configuration;
using Ryujinx.Input.HLE;
using Ryujinx.SDL2.Common;
using Ryujinx.SDL3.Common;
using SharpMetal.QuartzCore;
using System.Runtime.Versioning;
using static SDL2.SDL;
using static SDL3.SDL;
namespace Ryujinx.Headless
{
@@ -35,7 +35,7 @@ namespace Ryujinx.Headless
_caMetalLayer = new CAMetalLayer(SDL_Metal_GetLayer(SDL_Metal_CreateView(WindowHandle)));
}
SDL2Driver.MainThreadDispatcher?.Invoke(CreateLayer);
SDL3Driver.MainThreadDispatcher?.Invoke(CreateLayer);
}
protected override void InitializeRenderer() { }

View File

@@ -5,15 +5,15 @@ using Ryujinx.Common.Logging;
using Ryujinx.Graphics.OpenGL;
using Ryujinx.Input.HLE;
using System;
using static SDL2.SDL;
using static SDL3.SDL;
namespace Ryujinx.Headless
{
class OpenGLWindow : WindowBase
{
private static void CheckResult(int result)
private static void CheckResult(bool result)
{
if (result < 0)
if (!result)
{
throw new InvalidOperationException($"SDL_GL function returned an error: {SDL_GetError()}");
}
@@ -21,21 +21,21 @@ namespace Ryujinx.Headless
private static void SetupOpenGLAttributes(bool sharedContext, GraphicsDebugLevel debugLevel)
{
CheckResult(SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MAJOR_VERSION, 3));
CheckResult(SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_MINOR_VERSION, 3));
CheckResult(SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_PROFILE_MASK, SDL_GLprofile.SDL_GL_CONTEXT_PROFILE_COMPATIBILITY));
CheckResult(SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_CONTEXT_FLAGS, debugLevel != GraphicsDebugLevel.None ? (int)SDL_GLcontext.SDL_GL_CONTEXT_DEBUG_FLAG : 0));
CheckResult(SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_SHARE_WITH_CURRENT_CONTEXT, sharedContext ? 1 : 0));
CheckResult(SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MAJOR_VERSION, 3));
CheckResult(SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_MINOR_VERSION, 3));
CheckResult(SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_PROFILE_MASK, (int)SDL_GLProfile.SDL_GL_CONTEXT_PROFILE_COMPATIBILITY));
CheckResult(SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_CONTEXT_FLAGS, debugLevel != GraphicsDebugLevel.None ? (int)SDL_GLcontext.SDL_GL_CONTEXT_DEBUG_FLAG : 0));
CheckResult(SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_SHARE_WITH_CURRENT_CONTEXT, sharedContext ? 1 : 0));
CheckResult(SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_ACCELERATED_VISUAL, 1));
CheckResult(SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_RED_SIZE, 8));
CheckResult(SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_GREEN_SIZE, 8));
CheckResult(SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_BLUE_SIZE, 8));
CheckResult(SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_ALPHA_SIZE, 8));
CheckResult(SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_DEPTH_SIZE, 16));
CheckResult(SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_STENCIL_SIZE, 0));
CheckResult(SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_DOUBLEBUFFER, 1));
CheckResult(SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_STEREO, 0));
CheckResult(SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_ACCELERATED_VISUAL, 1));
CheckResult(SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_RED_SIZE, 8));
CheckResult(SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_GREEN_SIZE, 8));
CheckResult(SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_BLUE_SIZE, 8));
CheckResult(SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_ALPHA_SIZE, 8));
CheckResult(SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_DEPTH_SIZE, 16));
CheckResult(SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_STENCIL_SIZE, 0));
CheckResult(SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_DOUBLEBUFFER, 1));
CheckResult(SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_STEREO, 0));
}
private class OpenToolkitBindingsContext : IBindingsContext
@@ -46,35 +46,35 @@ namespace Ryujinx.Headless
}
}
private class SDL2OpenGLContext : IOpenGLContext
private class SDL3OpenGLContext : IOpenGLContext
{
private readonly nint _context;
private readonly nint _window;
private readonly bool _shouldDisposeWindow;
public SDL2OpenGLContext(nint context, nint window, bool shouldDisposeWindow = true)
public SDL3OpenGLContext(nint context, nint window, bool shouldDisposeWindow = true)
{
_context = context;
_window = window;
_shouldDisposeWindow = shouldDisposeWindow;
}
public static SDL2OpenGLContext CreateBackgroundContext(SDL2OpenGLContext sharedContext)
public static SDL3OpenGLContext CreateBackgroundContext(SDL3OpenGLContext sharedContext)
{
sharedContext.MakeCurrent();
// Ensure we share our contexts.
SetupOpenGLAttributes(true, GraphicsDebugLevel.None);
nint windowHandle = SDL_CreateWindow("Ryujinx background context window", 0, 0, 1, 1, SDL_WindowFlags.SDL_WINDOW_OPENGL | SDL_WindowFlags.SDL_WINDOW_HIDDEN);
nint windowHandle = SDL_CreateWindow("Ryujinx background context window", 1, 1, SDL_WindowFlags.SDL_WINDOW_OPENGL | SDL_WindowFlags.SDL_WINDOW_HIDDEN);
nint context = SDL_GL_CreateContext(windowHandle);
GL.LoadBindings(new OpenToolkitBindingsContext());
CheckResult(SDL_GL_SetAttribute(SDL_GLattr.SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 0));
CheckResult(SDL_GL_SetAttribute(SDL_GLAttr.SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 0));
CheckResult(SDL_GL_MakeCurrent(windowHandle, nint.Zero));
return new SDL2OpenGLContext(context, windowHandle);
return new SDL3OpenGLContext(context, windowHandle);
}
public void MakeCurrent()
@@ -84,9 +84,7 @@ namespace Ryujinx.Headless
return;
}
int res = SDL_GL_MakeCurrent(_window, _context);
if (res != 0)
if (!SDL_GL_MakeCurrent(_window, _context))
{
string errorMessage = $"SDL_GL_CreateContext failed with error \"{SDL_GetError()}\"";
@@ -100,7 +98,7 @@ namespace Ryujinx.Headless
public void Dispose()
{
SDL_GL_DeleteContext(_context);
SDL_GL_DestroyContext(_context);
if (_shouldDisposeWindow)
{
@@ -108,8 +106,8 @@ namespace Ryujinx.Headless
}
}
}
private SDL2OpenGLContext _openGLContext;
private SDL3OpenGLContext _openGLContext;
public OpenGLWindow(
InputManager inputManager,
@@ -141,10 +139,10 @@ namespace Ryujinx.Headless
}
// NOTE: The window handle needs to be disposed by the thread that created it and is handled separately.
_openGLContext = new SDL2OpenGLContext(context, WindowHandle, false);
_openGLContext = new SDL3OpenGLContext(context, WindowHandle, false);
// First take exclusivity on the OpenGL context.
((OpenGLRenderer)Renderer).InitializeBackgroundContext(SDL2OpenGLContext.CreateBackgroundContext(_openGLContext));
((OpenGLRenderer)Renderer).InitializeBackgroundContext(SDL3OpenGLContext.CreateBackgroundContext(_openGLContext));
_openGLContext.MakeCurrent();
@@ -160,7 +158,7 @@ namespace Ryujinx.Headless
else if (IsFullscreen)
{
// NOTE: grabbing the main display's dimensions directly as OpenGL doesn't scale along like the VulkanWindow.
if (SDL_GetDisplayBounds(DisplayId, out SDL_Rect displayBounds) < 0)
if (!SDL_GetDisplayBounds((uint)DisplayId, out SDL_Rect displayBounds))
{
Logger.Warning?.Print(LogClass.Application, $"Could not retrieve display bounds: {SDL_GetError()}");

View File

@@ -1,10 +1,10 @@
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Logging;
using Ryujinx.Input.HLE;
using Ryujinx.SDL2.Common;
using Ryujinx.SDL3.Common;
using System;
using System.Runtime.InteropServices;
using static SDL2.SDL;
using static SDL3.SDL;
namespace Ryujinx.Headless
{
@@ -50,7 +50,7 @@ namespace Ryujinx.Headless
void CreateSurface()
{
if (SDL_Vulkan_CreateSurface(WindowHandle, instance, out surfaceHandle) == SDL_bool.SDL_FALSE)
if (!SDL_Vulkan_CreateSurface(WindowHandle, instance, IntPtr.Zero, out surfaceHandle))
{
string errorMessage = $"SDL_Vulkan_CreateSurface failed with error \"{SDL_GetError()}\"";
@@ -60,9 +60,9 @@ namespace Ryujinx.Headless
}
}
if (SDL2Driver.MainThreadDispatcher != null)
if (SDL3Driver.MainThreadDispatcher != null)
{
SDL2Driver.MainThreadDispatcher(CreateSurface);
SDL3Driver.MainThreadDispatcher(CreateSurface);
}
else
{
@@ -74,23 +74,19 @@ namespace Ryujinx.Headless
public unsafe string[] GetRequiredInstanceExtensions()
{
if (SDL_Vulkan_GetInstanceExtensions(WindowHandle, out uint extensionsCount, nint.Zero) == SDL_bool.SDL_TRUE)
nint rawExtensions = SDL_Vulkan_GetInstanceExtensions(out uint count);
IntPtr[] extensionPointers = new IntPtr[count];
Marshal.Copy(rawExtensions, extensionPointers, 0, (int)count);
if (rawExtensions != nint.Zero)
{
nint[] rawExtensions = new nint[(int)extensionsCount];
string[] extensions = new string[(int)extensionsCount];
fixed (nint* rawExtensionsPtr = rawExtensions)
string[] extensions = new string[(int)count];
for (int i = 0; i < extensions.Length; i++)
{
if (SDL_Vulkan_GetInstanceExtensions(WindowHandle, out extensionsCount, (nint)rawExtensionsPtr) == SDL_bool.SDL_TRUE)
{
for (int i = 0; i < extensions.Length; i++)
{
extensions[i] = Marshal.PtrToStringUTF8(rawExtensions[i]);
}
return extensions;
}
extensions[i] = Marshal.PtrToStringUTF8(extensionPointers[i]);
}
return extensions;
}
string errorMessage = $"SDL_Vulkan_GetInstanceExtensions failed with error \"{SDL_GetError()}\"";

View File

@@ -1,6 +1,6 @@
using Humanizer;
using LibHac.Util;
using Ryujinx.Ava;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Configuration.Hid;
@@ -14,8 +14,8 @@ using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationPr
using Ryujinx.HLE.UI;
using Ryujinx.Input;
using Ryujinx.Input.HLE;
using Ryujinx.Input.SDL2;
using Ryujinx.SDL2.Common;
using Ryujinx.Input.SDL3;
using Ryujinx.SDL3.Common;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
@@ -23,7 +23,7 @@ using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using static SDL2.SDL;
using static SDL3.SDL;
using AntiAliasing = Ryujinx.Common.Configuration.AntiAliasing;
using ScalingFilter = Ryujinx.Common.Configuration.ScalingFilter;
using Switch = Ryujinx.HLE.Switch;
@@ -36,7 +36,7 @@ namespace Ryujinx.Headless
protected const int DefaultWidth = 1280;
protected const int DefaultHeight = 720;
private const int TargetFps = 60;
private SDL_WindowFlags DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI | SDL_WindowFlags.SDL_WINDOW_RESIZABLE | SDL_WindowFlags.SDL_WINDOW_INPUT_FOCUS | SDL_WindowFlags.SDL_WINDOW_SHOWN;
private SDL_WindowFlags DefaultFlags = SDL_WindowFlags.SDL_WINDOW_HIGH_PIXEL_DENSITY | SDL_WindowFlags.SDL_WINDOW_RESIZABLE | SDL_WindowFlags.SDL_WINDOW_INPUT_FOCUS;
private SDL_WindowFlags FullscreenFlag = 0;
private static readonly ConcurrentQueue<Action> _mainThreadActions = new();
@@ -69,7 +69,7 @@ namespace Ryujinx.Headless
public ScalingFilter ScalingFilter { get; set; }
public int ScalingFilterLevel { get; set; }
protected SDL2MouseDriver MouseDriver;
protected SDL3MouseDriver MouseDriver;
private readonly InputManager _inputManager;
private readonly IKeyboard _keyboardInterface;
protected readonly GraphicsDebugLevel GlLogLevel;
@@ -98,7 +98,7 @@ namespace Ryujinx.Headless
HideCursorMode hideCursorMode,
bool ignoreControllerApplet)
{
MouseDriver = new SDL2MouseDriver(hideCursorMode);
MouseDriver = new SDL3MouseDriver(hideCursorMode);
_inputManager = inputManager;
_inputManager.SetMouseDriver(MouseDriver);
NpadManager = _inputManager.CreateNpadManager();
@@ -115,7 +115,7 @@ namespace Ryujinx.Headless
_ignoreControllerApplet = ignoreControllerApplet;
HostUITheme = new HeadlessHostUiTheme();
SDL2Driver.Instance.Initialize();
SDL3Driver.Instance.Initialize();
}
public void Initialize(Switch device, List<InputConfig> inputConfigs, bool enableKeyboard, bool enableMouse)
@@ -154,11 +154,11 @@ namespace Ryujinx.Headless
{
fixed (byte* iconPtr = iconBytes)
{
nint rwOpsStruct = SDL_RWFromConstMem((nint)iconPtr, iconBytes.Length);
nint iconHandle = SDL_LoadBMP_RW(rwOpsStruct, 1);
nint rwOpsStruct = SDL_IOFromConstMem((nint)iconPtr, (nuint)iconBytes.Length);
SDL_Surface* iconHandle = SDL_LoadBMP_IO(rwOpsStruct, true);
SDL_SetWindowIcon(WindowHandle, iconHandle);
SDL_FreeSurface(iconHandle);
SDL_SetWindowIcon(WindowHandle, (nint)iconHandle);
SDL_DestroySurface((nint)iconHandle);
}
}
}
@@ -182,16 +182,16 @@ namespace Ryujinx.Headless
Width = ExclusiveFullscreenWidth;
Height = ExclusiveFullscreenHeight;
DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI;
DefaultFlags = SDL_WindowFlags.SDL_WINDOW_HIGH_PIXEL_DENSITY;
FullscreenFlag = SDL_WindowFlags.SDL_WINDOW_FULLSCREEN;
}
else if (IsFullscreen)
{
DefaultFlags = SDL_WindowFlags.SDL_WINDOW_ALLOW_HIGHDPI;
FullscreenFlag = SDL_WindowFlags.SDL_WINDOW_FULLSCREEN_DESKTOP;
DefaultFlags = SDL_WindowFlags.SDL_WINDOW_HIGH_PIXEL_DENSITY;
FullscreenFlag = SDL_WindowFlags.SDL_WINDOW_FULLSCREEN;
}
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);
WindowHandle = SDL_CreateWindow($"Ryujinx {Program.Version}{titleNameSection}{titleVersionSection}{titleIdSection}{titleArchSection}", Width, Height, DefaultFlags | FullscreenFlag | WindowFlags);
if (WindowHandle == nint.Zero)
{
@@ -205,16 +205,18 @@ namespace Ryujinx.Headless
SetWindowIcon();
_windowId = SDL_GetWindowID(WindowHandle);
SDL2Driver.Instance.RegisterWindow(_windowId, HandleWindowEvent);
SDL3Driver.Instance.RegisterWindow(_windowId, HandleWindowEvent);
}
private void HandleWindowEvent(SDL_Event evnt)
{
if (evnt.type == SDL_EventType.SDL_WINDOWEVENT)
if (evnt.type >= (uint)SDL_EventType.SDL_EVENT_WINDOW_FIRST &&
evnt.type <= (uint)SDL_EventType.SDL_EVENT_WINDOW_LAST)
{
switch (evnt.window.windowEvent)
switch (evnt.window.type)
{
case SDL_WindowEventID.SDL_WINDOWEVENT_SIZE_CHANGED:
case SDL_EventType.SDL_EVENT_WINDOW_RESIZED:
case SDL_EventType.SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
// Unlike on Windows, this event fires on macOS when triggering fullscreen mode.
// And promptly crashes the process because `Renderer?.window.SetSize` is undefined.
// As we don't need this to fire in either case we can test for fullscreen.
@@ -227,7 +229,7 @@ namespace Ryujinx.Headless
}
break;
case SDL_WindowEventID.SDL_WINDOWEVENT_CLOSE:
case SDL_EventType.SDL_EVENT_WINDOW_CLOSE_REQUESTED:
Exit();
break;
}
@@ -407,7 +409,7 @@ namespace Ryujinx.Headless
// Get screen touch position
if (!_enableMouse)
{
hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as SDL2MouseDriver).IsButtonPressed(MouseButton.Button1), _aspectRatio.ToFloat());
hasTouch = TouchScreenManager.Update(true, (_inputManager.MouseDriver as SDL3MouseDriver).IsButtonPressed(MouseButton.Button1), _aspectRatio.ToFloat());
}
if (!hasTouch)
@@ -514,25 +516,33 @@ namespace Ryujinx.Headless
public bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText)
{
SDL_MessageBoxData data = new()
SDL_MessageBoxButtonData[] buttons = new SDL_MessageBoxButtonData[buttonsText.Length];
unsafe
{
title = title,
message = message,
buttons = new SDL_MessageBoxButtonData[buttonsText.Length],
numbuttons = buttonsText.Length,
window = WindowHandle,
};
for (int i = 0; i < buttonsText.Length; i++)
{
data.buttons[i] = new SDL_MessageBoxButtonData
for (int i = 0; i < buttonsText.Length; i++)
{
buttonid = i,
text = buttonsText[i],
};
}
fixed (byte* button = buttonsText[i].ToBytes())
{
buttons[i] = new SDL_MessageBoxButtonData { buttonID = i, text = button, };
}
}
SDL_ShowMessageBox(ref data, out int _);
fixed (byte* t = title.ToBytes())
fixed (byte* m = message.ToBytes())
fixed (SDL_MessageBoxButtonData* b = buttons)
{
SDL_MessageBoxData data = new()
{
title = t,
message = m,
buttons = b,
numbuttons = buttonsText.Length,
window = WindowHandle,
};
SDL_ShowMessageBox(ref data, out int _);
}
}
return true;
}
@@ -550,11 +560,11 @@ namespace Ryujinx.Headless
TouchScreenManager?.Dispose();
NpadManager.Dispose();
SDL2Driver.Instance.UnregisterWindow(_windowId);
SDL3Driver.Instance.UnregisterWindow(_windowId);
SDL_DestroyWindow(WindowHandle);
SDL2Driver.Instance.Dispose();
SDL3Driver.Instance.Dispose();
}
}

View File

@@ -1,6 +1,5 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Keyboard;
using Ryujinx.Common.Logging;
using Ryujinx.Input;
using System;
using System.Collections.Generic;
@@ -144,11 +143,6 @@ namespace Ryujinx.Ava.Input
}
}
public void SetLed(uint packedRgb)
{
Logger.Info?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard");
}
public void SetTriggerThreshold(float triggerThreshold) { }
public void Rumble(float lowFrequency, float highFrequency, uint durationMs) { }

View File

@@ -59,8 +59,6 @@ namespace Ryujinx.Ava.Input
return new AvaloniaKeyboard(this, _keyboardIdentifers[0], LocaleManager.Instance[LocaleKeys.AllKeyboards]);
}
public IEnumerable<IGamepad> GetGamepads() => [GetGamepad("0")];
protected virtual void Dispose(bool disposing)
{
if (disposing)

View File

@@ -1,5 +1,4 @@
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Logging;
using Ryujinx.Input;
using System;
using System.Drawing;
@@ -75,11 +74,6 @@ namespace Ryujinx.Ava.Input
throw new NotImplementedException();
}
public void SetLed(uint packedRgb)
{
Logger.Info?.Print(LogClass.UI, "SetLed called on an AvaloniaMouse");
}
public void SetTriggerThreshold(float triggerThreshold)
{
throw new NotImplementedException();

View File

@@ -3,7 +3,6 @@ using Avalonia.Controls;
using Avalonia.Input;
using Ryujinx.Input;
using System;
using System.Collections.Generic;
using System.Numerics;
using MouseButton = Ryujinx.Input.MouseButton;
using Size = System.Drawing.Size;
@@ -135,8 +134,6 @@ namespace Ryujinx.Ava.Input
return new AvaloniaMouse(this);
}
public IEnumerable<IGamepad> GetGamepads() => [GetGamepad("0")];
public void Dispose()
{
if (_isDisposed)

View File

@@ -5,11 +5,9 @@ using Gommon;
using Projektanker.Icons.Avalonia;
using Projektanker.Icons.Avalonia.FontAwesome;
using Projektanker.Icons.Avalonia.MaterialDesign;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Ava.Utilities.SystemInfo;
using Ryujinx.Common;
@@ -19,7 +17,7 @@ using Ryujinx.Common.Logging;
using Ryujinx.Common.SystemInterop;
using Ryujinx.Graphics.Vulkan.MoltenVK;
using Ryujinx.Headless;
using Ryujinx.SDL2.Common;
using Ryujinx.SDL3.Common;
using System;
using System.Collections.Generic;
using System.IO;
@@ -47,9 +45,9 @@ namespace Ryujinx.Ava
{
Version = ReleaseInformation.Version;
if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041))
if (OperatingSystem.IsWindows() && !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 17134))
{
_ = MessageBoxA(nint.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 20H1 and newer.\n", $"Ryujinx {Version}", MbIconwarning);
_ = MessageBoxA(nint.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 1803 and newer.\n", $"Ryujinx {Version}", MbIconwarning);
}
PreviewerDetached = true;
@@ -126,9 +124,9 @@ namespace Ryujinx.Ava
// Initialize Discord integration.
DiscordIntegrationModule.Initialize();
// Initialize SDL2 driver
SDL2Driver.MainThreadDispatcher = action => Dispatcher.UIThread.InvokeAsync(action, DispatcherPriority.Input);
// Initialize SDL3 driver
SDL3Driver.MainThreadDispatcher = action => Dispatcher.UIThread.InvokeAsync(action, DispatcherPriority.Input);
ReloadConfig();
WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor();
@@ -230,16 +228,13 @@ namespace Ryujinx.Ava
internal static void PrintSystemInfo()
{
Logger.Notice.Print(LogClass.Application, $"{RyujinxApp.FullAppName} Version: {Version}");
Logger.Notice.Print(LogClass.Application, $".NET Runtime: {RuntimeInformation.FrameworkDescription}");
SystemInfo.Gather().Print();
Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {
Logger.GetEnabledLevels()
.FormatCollection(
x => x.ToString(),
separator: ", ",
emptyCollectionFallback: "<None>")
}");
var enabledLogLevels = Logger.GetEnabledLevels().ToArray();
Logger.Notice.Print(LogClass.Application, $"Logs Enabled: {(enabledLogLevels.Length is 0
? "<None>"
: enabledLogLevels.JoinToString(", "))}");
Logger.Notice.Print(LogClass.Application,
AppDataManager.Mode == AppDataManager.LaunchMode.Custom

View File

@@ -70,12 +70,12 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL2\Ryujinx.Audio.Backends.SDL2.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" />
<ProjectReference Include="..\Ryujinx.Graphics.Metal\Ryujinx.Graphics.Metal.csproj" />
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
<ProjectReference Include="..\Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj" />
<ProjectReference Include="..\Ryujinx.Input.SDL3\Ryujinx.Input.SDL3.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" />
<ProjectReference Include="..\Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj" />
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
@@ -123,16 +123,17 @@
<Generator>MSBuild:Compile</Generator>
</AvaloniaResource>
<AvaloniaResource Include="Assets\Styles\Styles.xaml" />
<AvaloniaResource Include="Assets\Styles\CheckboxMenuItemStyle.axaml" />
</ItemGroup>
<ItemGroup>
<None Remove="Assets\locales.json" />
<None Remove="Assets\Styles\Styles.xaml" />
<None Remove="Assets\Styles\Themes.xaml" />
<None Remove="Assets\Styles\CheckboxMenuItemStyle.xaml" />
<None Remove="Assets\Icons\Controller_JoyConLeft.svg" />
<None Remove="Assets\Icons\Controller_JoyConPair.svg" />
<None Remove="Assets\Icons\Controller_JoyConRight.svg" />
<None Remove="Assets\Icons\Controller_JoyConLeft_Settings.svg" />
<None Remove="Assets\Icons\Controller_JoyConRight_Settings.svg" />
<None Remove="Assets\Icons\Controller_ProCon.svg" />
</ItemGroup>
@@ -151,10 +152,11 @@
</EmbeddedResource>
<EmbeddedResource Include="Assets\locales.json" />
<EmbeddedResource Include="Assets\Styles\Styles.xaml" />
<EmbeddedResource Include="Assets\Styles\CheckboxMenuItemStyle.axaml" />
<EmbeddedResource Include="Assets\Icons\Controller_JoyConLeft.svg" />
<EmbeddedResource Include="Assets\Icons\Controller_JoyConPair.svg" />
<EmbeddedResource Include="Assets\Icons\Controller_JoyConRight.svg" />
<EmbeddedResource Include="Assets\Icons\Controller_JoyConLeft_Settings.svg" />
<EmbeddedResource Include="Assets\Icons\Controller_JoyConRight_Settings.svg" />
<EmbeddedResource Include="Assets\Icons\Controller_ProCon.svg" />
<EmbeddedResource Include="Assets\UIImages\Icon_NCA.png" />
<EmbeddedResource Include="Assets\UIImages\Icon_NRO.png" />
@@ -174,6 +176,12 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Assets\Fonts\Mono\" />
<Folder Include="bin\Debug\net9.0\" />
</ItemGroup>
<ItemGroup>
<Compile Update="UI\Applet\UserSelectorDialog.axaml.cs">
<DependentUpon>UserSelectorDialog.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
</Project>

View File

@@ -16,6 +16,5 @@
<Application.Styles>
<sty:FluentAvaloniaTheme PreferUserAccentColor="True" PreferSystemTheme="False" />
<StyleInclude Source="/Assets/Styles/Styles.xaml" />
<StyleInclude Source="/Assets/Styles/CheckboxMenuItemStyle.axaml"/>
</Application.Styles>
</Application>

View File

@@ -6,6 +6,7 @@ using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.ViewModels.Input;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities.Configuration;
using Ryujinx.Common;
@@ -41,7 +42,7 @@ namespace Ryujinx.Ava.UI.Applet
bool okPressed = false;
if (ConfigurationState.Instance.System.IgnoreApplet)
if (ConfigurationState.Instance.IgnoreApplet)
return false;
Dispatcher.UIThread.InvokeAsync(async () =>

View File

@@ -106,10 +106,6 @@
Click="ExtractApplicationRomFs_Click"
Header="{ext:Locale GameListContextMenuExtractDataRomFS}"
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" />
<MenuItem
Click="ExtractAocRomFs_Click"
Header="{ext:Locale GameListContextMenuExtractDataAocRomFS}"
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataAocRomFSToolTip}" />
<MenuItem
Click="ExtractApplicationLogo_Click"
Header="{ext:Locale GameListContextMenuExtractDataLogo}"

View File

@@ -3,13 +3,10 @@ using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using LibHac;
using LibHac.Fs;
using LibHac.Tools.FsSystem.NcaUtils;
using Ryujinx.Ava.Common;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.UI.Windows;
@@ -17,7 +14,6 @@ using Ryujinx.Ava.Utilities;
using Ryujinx.Ava.Utilities.AppLibrary;
using Ryujinx.Common.Configuration;
using Ryujinx.Common.Helper;
using Ryujinx.Common.Logging;
using Ryujinx.HLE.HOS;
using SkiaSharp;
using System;
@@ -251,7 +247,7 @@ namespace Ryujinx.Ava.UI.Controls
{
if (sender is MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
{
string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString.ToLower(), "cache", "shader");
string shaderCacheDir = Path.Combine(AppDataManager.GamesDirPath, viewModel.SelectedApplication.IdString, "cache", "shader");
if (!Directory.Exists(shaderCacheDir))
{
@@ -283,22 +279,6 @@ namespace Ryujinx.Ava.UI.Controls
viewModel.SelectedApplication.Path,
viewModel.SelectedApplication.Name);
}
public async void ExtractAocRomFs_Click(object sender, RoutedEventArgs args)
{
if (sender is not MenuItem { DataContext: MainWindowViewModel { SelectedApplication: not null } viewModel })
return;
DownloadableContentModel selectedDlc = await DlcSelectView.Show(viewModel.SelectedApplication.IdBase, viewModel.ApplicationLibrary);
if (selectedDlc is not null)
{
await ApplicationHelper.ExtractAoc(
viewModel.StorageProvider,
selectedDlc.ContainerPath,
selectedDlc.FileName);
}
}
public async void ExtractApplicationLogo_Click(object sender, RoutedEventArgs args)
{

View File

@@ -2,10 +2,11 @@
x:Class="Ryujinx.Ava.UI.Controls.ApplicationListView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
d:DesignHeight="450"
d:DesignWidth="800"
Focusable="True"

View File

@@ -1,67 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:helpers="using:Ryujinx.Ava.UI.Helpers"
xmlns:ext="using:Ryujinx.Ava.Common.Markup"
xmlns:models="using:Ryujinx.Ava.Common.Models"
xmlns:viewModels="using:Ryujinx.Ava.UI.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ryujinx.Ava.UI.Controls.DlcSelectView"
x:DataType="viewModels:DlcSelectViewModel">
<Grid RowDefinitions="*,Auto,*">
<TextBlock
Classes="h1"
Margin="5, -5, 0, 15"
HorizontalAlignment="Center"
VerticalAlignment="Center"
TextWrapping="Wrap"
Text="{ext:Locale ExtractAocListHeader}" />
<ScrollViewer Grid.Row="2">
<ListBox
AutoScrollToSelectedItem="False"
SelectionMode="Single"
Background="Transparent"
SelectedItem="{Binding SelectedDlc}"
ItemsSource="{Binding Dlcs}">
<ListBox.DataTemplates>
<DataTemplate
DataType="models:DownloadableContentModel">
<Panel Margin="10" Background="Transparent">
<Grid ColumnDefinitions="*,Auto">
<Grid
Grid.Column="0" ColumnDefinitions="*,Auto">
<TextBlock
Grid.Column="0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
MaxLines="2"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis">
<TextBlock.Text>
<MultiBinding Converter="{x:Static helpers:DownloadableContentLabelConverter.Instance}">
<Binding Path="FileName" />
<Binding Path="IsBundled" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
<TextBlock
Grid.Column="1"
Margin="10, 0, 0, 0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding TitleIdStr}" />
</Grid>
</Grid>
</Panel>
</DataTemplate>
</ListBox.DataTemplates>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Background" Value="Transparent" />
</Style>
</ListBox.Styles>
</ListBox>
</ScrollViewer>
</Grid>
</UserControl>

View File

@@ -1,51 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Ava.Utilities.AppLibrary;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Controls
{
public partial class DlcSelectView : UserControl
{
public DlcSelectView()
{
InitializeComponent();
}
#nullable enable
public static async Task<DownloadableContentModel?> Show(ulong selectedTitleId, ApplicationLibrary appLibrary)
#nullable disable
{
DlcSelectViewModel viewModel = new(selectedTitleId, appLibrary);
ContentDialog contentDialog = new()
{
PrimaryButtonText = LocaleManager.Instance[LocaleKeys.Continue],
SecondaryButtonText = string.Empty,
CloseButtonText = string.Empty,
Content = new DlcSelectView { DataContext = viewModel }
};
Style closeButton = new(x => x.Name("CloseButton"));
closeButton.Setters.Add(new Setter(WidthProperty, 80d));
Style closeButtonParent = new(x => x.Name("CommandSpace"));
closeButtonParent.Setters.Add(new Setter(HorizontalAlignmentProperty,
Avalonia.Layout.HorizontalAlignment.Right));
contentDialog.Styles.Add(closeButton);
contentDialog.Styles.Add(closeButtonParent);
await ContentDialogHelper.ShowAsync(contentDialog);
return viewModel.SelectedDlc;
}
}
}

View File

@@ -69,7 +69,7 @@ namespace Ryujinx.Ava.UI.Controls
VirtualFileSystem ownerVirtualFileSystem,
HorizonClient ownerHorizonClient)
{
NavigationDialogHost content = new(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient);
var content = new NavigationDialogHost(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient);
ContentDialog contentDialog = new()
{
Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle],

View File

@@ -12,9 +12,9 @@ namespace Ryujinx.Ava.UI.Helpers
public static RelayCommand CreateConditional(Action action, Func<bool> canExecute)
=> new(action, canExecute);
public static RelayCommand<T> Create<T>(Action<T?> action)
public static RelayCommand<T> CreateWithArg<T>(Action<T?> action)
=> new(action);
public static RelayCommand<T> CreateConditional<T>(Action<T?> action, Predicate<T?> canExecute)
public static RelayCommand<T> CreateConditionalWithArg<T>(Action<T?> action, Predicate<T?> canExecute)
=> new(action, canExecute);
public static AsyncRelayCommand Create(Func<Task> action)
@@ -24,11 +24,11 @@ namespace Ryujinx.Ava.UI.Helpers
public static AsyncRelayCommand CreateSilentFail(Func<Task> action)
=> new(action, AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler);
public static AsyncRelayCommand<T> Create<T>(Func<T?, Task> action)
public static AsyncRelayCommand<T> CreateWithArg<T>(Func<T?, Task> action)
=> new(action, AsyncRelayCommandOptions.None);
public static AsyncRelayCommand<T> CreateConcurrent<T>(Func<T?, Task> action)
public static AsyncRelayCommand<T> CreateConcurrentWithArg<T>(Func<T?, Task> action)
=> new(action, AsyncRelayCommandOptions.AllowConcurrentExecutions);
public static AsyncRelayCommand<T> CreateSilentFail<T>(Func<T?, Task> action)
public static AsyncRelayCommand<T> CreateSilentFailWithArg<T>(Func<T?, Task> action)
=> new(action, AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler);
public static AsyncRelayCommand CreateConditional(Func<Task> action, Func<bool> canExecute)
@@ -38,11 +38,11 @@ namespace Ryujinx.Ava.UI.Helpers
public static AsyncRelayCommand CreateSilentFailConditional(Func<Task> action, Func<bool> canExecute)
=> new(action, canExecute, AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler);
public static AsyncRelayCommand<T> CreateConditional<T>(Func<T?, Task> action, Predicate<T?> canExecute)
public static AsyncRelayCommand<T> CreateConditionalWithArg<T>(Func<T?, Task> action, Predicate<T?> canExecute)
=> new(action, canExecute, AsyncRelayCommandOptions.None);
public static AsyncRelayCommand<T> CreateConcurrentConditional<T>(Func<T?, Task> action, Predicate<T?> canExecute)
public static AsyncRelayCommand<T> CreateConcurrentConditionalWithArg<T>(Func<T?, Task> action, Predicate<T?> canExecute)
=> new(action, canExecute, AsyncRelayCommandOptions.AllowConcurrentExecutions);
public static AsyncRelayCommand<T> CreateSilentFailConditional<T>(Func<T?, Task> action, Predicate<T?> canExecute)
public static AsyncRelayCommand<T> CreateSilentFailConditionalWithArg<T>(Func<T?, Task> action, Predicate<T?> canExecute)
=> new(action, canExecute, AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler);
}
}

View File

@@ -1,4 +1,3 @@
using Avalonia.Media;
using Ryujinx.Ava.UI.ViewModels;
using Ryujinx.Common.Configuration.Hid;
using Ryujinx.Common.Configuration.Hid.Controller;
@@ -409,58 +408,6 @@ namespace Ryujinx.Ava.UI.Models.Input
OnPropertyChanged();
}
}
private bool _enableLedChanging;
public bool EnableLedChanging
{
get => _enableLedChanging;
set
{
_enableLedChanging = value;
OnPropertyChanged();
}
}
public bool ShowLedColorPicker => !TurnOffLed && !UseRainbowLed;
private bool _turnOffLed;
public bool TurnOffLed
{
get => _turnOffLed;
set
{
_turnOffLed = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ShowLedColorPicker));
}
}
private bool _useRainbowLed;
public bool UseRainbowLed
{
get => _useRainbowLed;
set
{
_useRainbowLed = value;
OnPropertyChanged();
OnPropertyChanged(nameof(ShowLedColorPicker));
}
}
private Color _ledColor;
public Color LedColor
{
get => _ledColor;
set
{
_ledColor = value;
OnPropertyChanged();
}
}
public GamepadInputConfig(InputConfig config)
{
@@ -536,28 +483,15 @@ namespace Ryujinx.Ava.UI.Models.Input
WeakRumble = controllerInput.Rumble.WeakRumble;
StrongRumble = controllerInput.Rumble.StrongRumble;
}
if (controllerInput.Led != null)
{
EnableLedChanging = controllerInput.Led.EnableLed;
TurnOffLed = controllerInput.Led.TurnOffLed;
UseRainbowLed = controllerInput.Led.UseRainbow;
uint rawColor = controllerInput.Led.LedColor;
byte alpha = (byte)(rawColor >> 24);
byte red = (byte)(rawColor >> 16);
byte green = (byte)(rawColor >> 8);
byte blue = (byte)(rawColor % 256);
LedColor = new Color(alpha, red, green, blue);
}
}
}
public InputConfig GetConfig()
{
StandardControllerInputConfig config = new()
var config = new StandardControllerInputConfig
{
Id = Id,
Backend = InputBackendType.GamepadSDL2,
Backend = InputBackendType.GamepadSDL3,
PlayerIndex = PlayerIndex,
ControllerType = ControllerType,
LeftJoycon = new LeftJoyconCommonConfig<GamepadInputId>
@@ -606,13 +540,6 @@ namespace Ryujinx.Ava.UI.Models.Input
WeakRumble = WeakRumble,
StrongRumble = StrongRumble,
},
Led = new LedConfigController
{
EnableLed = EnableLedChanging,
TurnOffLed = this.TurnOffLed,
UseRainbow = UseRainbowLed,
LedColor = LedColor.ToUInt32()
},
Version = InputConfig.CurrentVersion,
DeadzoneLeft = DeadzoneLeft,
DeadzoneRight = DeadzoneRight,

View File

@@ -432,7 +432,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
try
{
HttpResponseMessage response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, "https://raw.githubusercontent.com/Ryubing/Ryujinx/refs/heads/master/assets/amiibo/Amiibo.json"));
HttpResponseMessage response = await _httpClient.SendAsync(new HttpRequestMessage(HttpMethod.Head, "https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/Amiibo.json"));
if (response.IsSuccessStatusCode)
{
@@ -451,7 +451,7 @@ namespace Ryujinx.Ava.UI.ViewModels
{
try
{
HttpResponseMessage response = await _httpClient.GetAsync($"https://raw.githubusercontent.com/Ryubing/Ryujinx/refs/heads/master/assets/amiibo/Amiibo.json");
HttpResponseMessage response = await _httpClient.GetAsync($"https://raw.githubusercontent.com/GreemDev/Ryujinx/refs/heads/master/assets/amiibo/Amiibo.json");
if (response.IsSuccessStatusCode)
{

View File

@@ -1,25 +0,0 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ryujinx.Ava.Common.Models;
using Ryujinx.Ava.Utilities.AppLibrary;
using System.Linq;
namespace Ryujinx.Ava.UI.ViewModels
{
public partial class DlcSelectViewModel : BaseModel
{
[ObservableProperty] private DownloadableContentModel[] _dlcs;
#nullable enable
[ObservableProperty] private DownloadableContentModel? _selectedDlc;
#nullable disable
public DlcSelectViewModel(ulong titleId, ApplicationLibrary appLibrary)
{
_dlcs = appLibrary.DownloadableContents.Items
.Where(x => x.Dlc.TitleIdBase == titleId)
.Select(x => x.Dlc)
.OrderBy(it => it.IsBundled ? 0 : 1)
.ThenBy(it => it.TitleId)
.ToArray();
}
}
}

View File

@@ -1,8 +1,5 @@
using Avalonia.Svg.Skia;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models.Input;
using Ryujinx.Ava.UI.Views.Input;
@@ -13,6 +10,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
[ObservableProperty] private GamepadInputConfig _config;
private bool _isLeft;
public bool IsLeft
{
get => _isLeft;
@@ -25,6 +23,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
}
private bool _isRight;
public bool IsRight
{
get => _isRight;
@@ -40,7 +39,11 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
[ObservableProperty] private SvgImage _image;
public InputViewModel ParentModel { get; }
public readonly InputViewModel ParentModel;
[ObservableProperty] private string _leftStickPosition;
[ObservableProperty] private string _rightStickPosition;
public ControllerInputViewModel(InputViewModel model, GamepadInputConfig config)
{
@@ -60,21 +63,16 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
await RumbleInputView.Show(this);
}
public RelayCommand LedDisabledChanged => Commands.Create(() =>
{
if (!Config.EnableLedChanging) return;
if (Config.TurnOffLed)
ParentModel.SelectedGamepad.ClearLed();
else
ParentModel.SelectedGamepad.SetLed(Config.LedColor.ToUInt32());
});
public void OnParentModelChanged()
{
IsLeft = ParentModel.IsLeft;
IsRight = ParentModel.IsRight;
Image = ParentModel.Image;
}
public void UpdateImageCss(string css)
{
Image = new SvgImage { Source = ParentModel.Image.Source, Css = css };
}
}
}

View File

@@ -3,7 +3,6 @@ using Avalonia.Controls;
using Avalonia.Svg.Skia;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Gommon;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Input;
using Ryujinx.Ava.UI.Helpers;
@@ -37,8 +36,8 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
private const string Disabled = "disabled";
private const string ProControllerResource = "Ryujinx/Assets/Icons/Controller_ProCon.svg";
private const string JoyConPairResource = "Ryujinx/Assets/Icons/Controller_JoyConPair.svg";
private const string JoyConLeftResource = "Ryujinx/Assets/Icons/Controller_JoyConLeft.svg";
private const string JoyConRightResource = "Ryujinx/Assets/Icons/Controller_JoyConRight.svg";
private const string JoyConLeftResource = "Ryujinx/Assets/Icons/Controller_JoyConLeft_Settings.svg";
private const string JoyConRightResource = "Ryujinx/Assets/Icons/Controller_JoyConRight_Settings.svg";
private const string KeyboardString = "keyboard";
private const string ControllerString = "controller";
private readonly MainWindow _mainWindow;
@@ -55,18 +54,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
private static readonly InputConfigJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public IGamepadDriver AvaloniaKeyboardDriver { get; }
private IGamepad _selectedGamepad;
public IGamepad SelectedGamepad
{
get => _selectedGamepad;
private set
{
_selectedGamepad = value;
OnPropertiesChanged(nameof(HasLed), nameof(CanClearLed));
}
}
public IGamepad SelectedGamepad { get; private set; }
public ObservableCollection<PlayerModel> PlayerIndexes { get; set; }
public ObservableCollection<(DeviceType Type, string Id, string Name)> Devices { get; set; }
@@ -81,9 +69,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
public bool IsRight { get; set; }
public bool IsLeft { get; set; }
public bool HasLed => SelectedGamepad.Features.HasFlag(GamepadFeaturesFlag.Led);
public bool CanClearLed => SelectedGamepad.Name.ContainsIgnoreCase("DualSense");
public bool IsModified { get; set; }
public event Action NotifyChangesEvent;
@@ -198,7 +183,6 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
image.Source = source;
}
return image;
}
}
@@ -595,7 +579,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
config = new StandardControllerInputConfig
{
Version = InputConfig.CurrentVersion,
Backend = InputBackendType.GamepadSDL2,
Backend = InputBackendType.GamepadSDL3,
Id = id,
ControllerType = ControllerType.ProController,
DeadzoneLeft = 0.1f,

View File

@@ -38,6 +38,7 @@ using Ryujinx.HLE.HOS.Services.Account.Acc;
using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption;
using Ryujinx.HLE.UI;
using Ryujinx.Input.HLE;
using Silk.NET.Vulkan;
using SkiaSharp;
using System;
using System.Collections.Generic;
@@ -488,19 +489,6 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public bool StartGamesWithoutUI
{
get => ConfigurationState.Instance.UI.StartNoUI;
set
{
ConfigurationState.Instance.UI.StartNoUI.Value = value;
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
OnPropertyChanged();
}
}
public bool ShowConsole
{
get => ConfigurationState.Instance.UI.ShowConsole;
@@ -637,7 +625,6 @@ namespace Ryujinx.Ava.UI.ViewModels
ApplicationSort.FileSize => LocaleManager.Instance[LocaleKeys.GameListHeaderFileSize],
ApplicationSort.Path => LocaleManager.Instance[LocaleKeys.GameListHeaderPath],
ApplicationSort.Favorite => LocaleManager.Instance[LocaleKeys.CommonFavorite],
ApplicationSort.TitleId => LocaleManager.Instance[LocaleKeys.DlcManagerTableHeadingTitleIdLabel],
_ => string.Empty,
};
}
@@ -708,7 +695,6 @@ namespace Ryujinx.Ava.UI.ViewModels
public IHostUIHandler UiHandler { get; internal set; }
public bool IsSortedByFavorite => SortMode == ApplicationSort.Favorite;
public bool IsSortedByTitle => SortMode == ApplicationSort.Title;
public bool IsSortedByTitleId => SortMode == ApplicationSort.TitleId;
public bool IsSortedByDeveloper => SortMode == ApplicationSort.Developer;
public bool IsSortedByLastPlayed => SortMode == ApplicationSort.LastPlayed;
public bool IsSortedByTimePlayed => SortMode == ApplicationSort.TotalTimePlayed;
@@ -741,7 +727,6 @@ namespace Ryujinx.Ava.UI.ViewModels
ApplicationSort.FileSize => CreateComparer(IsAscending, app => app.FileSize),
ApplicationSort.Path => CreateComparer(IsAscending, app => app.Path),
ApplicationSort.Favorite => CreateComparer(IsAscending, app => new AppListFavoriteComparable(app)),
ApplicationSort.TitleId => CreateComparer(IsAscending, app => app.Id),
_ => null,
#pragma warning restore IDE0055
};
@@ -1211,11 +1196,6 @@ namespace Ryujinx.Ava.UI.ViewModels
StartGamesInFullscreen = !StartGamesInFullscreen;
}
public void ToggleStartGamesWithoutUI()
{
StartGamesWithoutUI = !StartGamesWithoutUI;
}
public void ToggleShowConsole()
{
ShowConsole = !ShowConsole;
@@ -1662,7 +1642,10 @@ namespace Ryujinx.Ava.UI.ViewModels
public async Task OpenAmiiboWindow()
{
if (AppHost.Device.System.SearchingForAmiibo(out int deviceId) && IsGameRunning)
if (!IsAmiiboRequested)
return;
if (AppHost.Device.System.SearchingForAmiibo(out int deviceId))
{
string titleId = AppHost.Device.Processes.ActiveApplication.ProgramIdText.ToUpper();
AmiiboWindow window = new(ShowAll, LastScannedAmiiboId, titleId);
@@ -1680,7 +1663,10 @@ namespace Ryujinx.Ava.UI.ViewModels
}
public async Task OpenBinFile()
{
if (AppHost.Device.System.SearchingForAmiibo(out _) && IsGameRunning)
if (!IsAmiiboRequested)
return;
if (AppHost.Device.System.SearchingForAmiibo(out int deviceId))
{
var result = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{

View File

@@ -5,7 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using Gommon;
using LibHac.Tools.FsSystem;
using Ryujinx.Audio.Backends.OpenAL;
using Ryujinx.Audio.Backends.SDL2;
using Ryujinx.Audio.Backends.SDL3;
using Ryujinx.Audio.Backends.SoundIo;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.UI.Helpers;
@@ -211,7 +211,7 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool EnableDebug { get; set; }
public bool IsOpenAlEnabled { get; set; }
public bool IsSoundIoEnabled { get; set; }
public bool IsSDL2Enabled { get; set; }
public bool IsSDL3Enabled { get; set; }
public bool IsCustomResolutionScaleActive => _resolutionScale == 4;
public bool IsScalingFilterActive => _scalingFilter == (int)Ryujinx.Common.Configuration.ScalingFilter.Fsr;
@@ -372,13 +372,13 @@ namespace Ryujinx.Ava.UI.ViewModels
{
IsOpenAlEnabled = OpenALHardwareDeviceDriver.IsSupported;
IsSoundIoEnabled = SoundIoHardwareDeviceDriver.IsSupported;
IsSDL2Enabled = SDL2HardwareDeviceDriver.IsSupported;
IsSDL3Enabled = SDL3HardwareDeviceDriver.IsSupported;
await Dispatcher.UIThread.InvokeAsync(() =>
{
OnPropertyChanged(nameof(IsOpenAlEnabled));
OnPropertyChanged(nameof(IsSoundIoEnabled));
OnPropertyChanged(nameof(IsSDL2Enabled));
OnPropertyChanged(nameof(IsSDL3Enabled));
});
}
@@ -488,6 +488,7 @@ namespace Ryujinx.Ava.UI.ViewModels
EnableDiscordIntegration = config.EnableDiscordIntegration;
CheckUpdatesOnStart = config.CheckUpdatesOnStart;
ShowConfirmExit = config.ShowConfirmExit;
IgnoreApplet = config.IgnoreApplet;
RememberWindowState = config.RememberWindowState;
ShowTitleBar = config.ShowTitleBar;
HideCursor = (int)config.HideCursor.Value;
@@ -531,7 +532,6 @@ namespace Ryujinx.Ava.UI.ViewModels
EnableFsIntegrityChecks = config.System.EnableFsIntegrityChecks;
DramSize = config.System.DramSize;
IgnoreMissingServices = config.System.IgnoreMissingServices;
IgnoreApplet = config.System.IgnoreApplet;
// CPU
EnablePptc = config.System.EnablePtc;
@@ -591,6 +591,7 @@ namespace Ryujinx.Ava.UI.ViewModels
config.EnableDiscordIntegration.Value = EnableDiscordIntegration;
config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart;
config.ShowConfirmExit.Value = ShowConfirmExit;
config.IgnoreApplet.Value = IgnoreApplet;
config.RememberWindowState.Value = RememberWindowState;
config.ShowTitleBar.Value = ShowTitleBar;
config.HideCursor.Value = (HideCursorMode)HideCursor;
@@ -631,10 +632,12 @@ namespace Ryujinx.Ava.UI.ViewModels
}
config.System.SystemTimeOffset.Value = Convert.ToInt64((CurrentDate.ToUnixTimeSeconds() + CurrentTime.TotalSeconds) - DateTimeOffset.Now.ToUnixTimeSeconds());
config.Graphics.VSyncMode.Value = VSyncMode;
config.Graphics.EnableCustomVSyncInterval.Value = EnableCustomVSyncInterval;
config.Graphics.CustomVSyncInterval.Value = CustomVSyncInterval;
config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks;
config.System.DramSize.Value = DramSize;
config.System.IgnoreMissingServices.Value = IgnoreMissingServices;
config.System.IgnoreApplet.Value = IgnoreApplet;
// CPU
config.System.EnablePtc.Value = EnablePptc;
@@ -643,9 +646,6 @@ namespace Ryujinx.Ava.UI.ViewModels
config.System.UseHypervisor.Value = UseHypervisor;
// Graphics
config.Graphics.VSyncMode.Value = VSyncMode;
config.Graphics.EnableCustomVSyncInterval.Value = EnableCustomVSyncInterval;
config.Graphics.CustomVSyncInterval.Value = CustomVSyncInterval;
config.Graphics.GraphicsBackend.Value = (GraphicsBackend)GraphicsBackendIndex;
config.Graphics.PreferredGpu.Value = _gpuIds.ElementAtOrDefault(PreferredGpuIndex);
config.Graphics.EnableShaderCache.Value = EnableShaderCache;

View File

@@ -4,7 +4,6 @@
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels.Input"
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
@@ -16,7 +15,10 @@
x:DataType="viewModels:ControllerInputViewModel"
x:CompileBindings="True"
mc:Ignorable="d"
Focusable="True">
Focusable="True"
Unloaded="Control_OnUnloaded"
>
<Design.DataContext>
<viewModels:ControllerInputViewModel />
</Design.DataContext>
@@ -184,6 +186,9 @@
<TextBlock
HorizontalAlignment="Center"
Text="{ext:Locale ControllerSettingsStickDeadzone}" />
<TextBlock
HorizontalAlignment="Center"
Text="{Binding LeftStickPosition }" />
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"
@@ -429,7 +434,7 @@
</StackPanel>
</StackPanel>
</Border>
<!-- Motion, Rumble, LED -->
<!-- Motion + Rumble -->
<StackPanel
Margin="0,10,0,0"
Spacing="5"
@@ -487,59 +492,6 @@
</Button>
</Grid>
</Border>
<Border
BorderBrush="{DynamicResource ThemeControlBorderColor}"
BorderThickness="1"
CornerRadius="5"
HorizontalAlignment="Stretch"
Margin="0,-1,0,0">
<Grid IsVisible="{Binding ParentModel.HasLed}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<CheckBox
Margin="10, 10, 5, 10"
MinWidth="0"
Grid.Column="0"
IsChecked="{Binding Config.EnableLedChanging, Mode=TwoWay}">
<TextBlock Text="{ext:Locale ControllerSettingsLedColor}" />
</CheckBox>
<CheckBox
Margin="5, 10, 5, 10"
MinWidth="0"
Grid.Column="1"
IsVisible="{Binding ParentModel.CanClearLed}"
IsChecked="{Binding Config.TurnOffLed, Mode=TwoWay}"
Command="{Binding LedDisabledChanged}">
<TextBlock Text="{ext:Locale ControllerSettingsLedColorDisable}" />
</CheckBox>
<CheckBox
Margin="5, 10 5,10"
MinWidth="0"
Grid.Column="2"
IsEnabled="{Binding !Config.TurnOffLed}"
IsChecked="{Binding Config.UseRainbowLed, Mode=TwoWay}">
<TextBlock Text="{ext:Locale ControllerSettingsLedColorRainbow}" />
</CheckBox>
<ui:ColorPickerButton
Grid.Column="3"
IsEnabled="{Binding Config.ShowLedColorPicker}"
Margin="5, 10, 10, 10"
IsMoreButtonVisible="False"
UseColorPalette="False"
UseColorTriangle="False"
UseColorWheel="False"
ShowAcceptDismissButtons="False"
IsAlphaEnabled="False"
AttachedToVisualTree="ColorPickerButton_OnAttachedToVisualTree"
ColorChanged="ColorPickerButton_OnColorChanged"
Color="{Binding Config.LedColor, Mode=TwoWay}">
</ui:ColorPickerButton>
</Grid>
</Border>
</StackPanel>
</StackPanel>
<!-- Right Controls -->
@@ -770,6 +722,9 @@
<TextBlock
HorizontalAlignment="Center"
Text="{ext:Locale ControllerSettingsStickDeadzone}" />
<TextBlock
HorizontalAlignment="Center"
Text="{Binding RightStickPosition }" />
<StackPanel
HorizontalAlignment="Center"
VerticalAlignment="Center"

View File

@@ -4,14 +4,18 @@ using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using FluentAvalonia.UI.Controls;
using Avalonia.Threading;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Models;
using Ryujinx.Ava.UI.Models.Input;
using Ryujinx.Ava.UI.ViewModels.Input;
using Ryujinx.Common.Configuration.Hid.Controller;
using Ryujinx.HLE.HOS.Services.Hid;
using Ryujinx.Input;
using Ryujinx.Input.Assigner;
using System.Linq;
using Ryujinx.Input.HLE;
using System;
using System.Text;
using System.Threading.Tasks;
using StickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
namespace Ryujinx.Ava.UI.Views.Input
@@ -19,6 +23,7 @@ namespace Ryujinx.Ava.UI.Views.Input
public partial class ControllerInputView : UserControl
{
private ButtonKeyAssigner _currentAssigner;
private volatile bool _isRunning = true;
public ControllerInputView()
{
@@ -39,6 +44,8 @@ namespace Ryujinx.Ava.UI.Views.Input
break;
}
}
StartUpdatingData();
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
@@ -236,31 +243,72 @@ namespace Ryujinx.Ava.UI.Views.Input
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
foreach (IGamepad gamepad in RyujinxApp.MainWindow.InputManager.GamepadDriver.GetGamepads())
{
gamepad?.ClearLed();
}
_currentAssigner?.Cancel();
_currentAssigner = null;
}
private void ColorPickerButton_OnColorChanged(ColorPickerButton sender, ColorButtonColorChangedEventArgs args)
private void Control_OnUnloaded(object sender, RoutedEventArgs e)
{
if (!args.NewColor.HasValue) return;
if (DataContext is not ControllerInputViewModel cVm) return;
if (!cVm.Config.EnableLedChanging) return;
cVm.ParentModel.SelectedGamepad.SetLed(args.NewColor.Value.ToUInt32());
_isRunning = false;
}
private void ColorPickerButton_OnAttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
private string BuildSvgCss(IGamepad gamepad, GamepadInputConfig config, JoystickPosition leftPosition,
JoystickPosition rightPosition)
{
if (DataContext is not ControllerInputViewModel cVm) return;
if (!cVm.Config.EnableLedChanging) return;
gamepad.SetConfiguration(config.GetConfig());
StringBuilder sb = new();
for (var i = 0; i < (int)GamepadInputId.Count; i++)
{
var button = (GamepadButtonInputId)i;
if (gamepad.GetMappedStateSnapshot().IsPressed(button))
{
sb.Append($"#{button}{{fill:#00bbdb;}}");
}
}
cVm.ParentModel.SelectedGamepad.SetLed(cVm.Config.LedColor.ToUInt32());
sb.Append(
$"#LeftStick{{transform: translate ({(float)leftPosition.Dx / short.MaxValue * 10} {-(float)leftPosition.Dy / short.MaxValue * 10});}}");
sb.Append(
$"#RightStick{{transform: translate ({(float)rightPosition.Dx / short.MaxValue * 10} {-(float)rightPosition.Dy / short.MaxValue * 10});}}");
return sb.ToString();
}
private void StartUpdatingData()
{
Dispatcher.UIThread.InvokeAsync(async () =>
{
while (_isRunning)
{
var viewModel = DataContext as ControllerInputViewModel;
if (viewModel != null)
{
IGamepad gamepad = viewModel.ParentModel.SelectedGamepad;
var config = viewModel.Config;
JoystickPosition leftPosition = default, rightposition = default;
if (config.LeftJoystick != StickInputId.Unbound)
{
var stickInputId = (Ryujinx.Input.StickInputId)(int)config.LeftJoystick;
(float leftAxisX, float leftAxisY) = gamepad.GetStick(stickInputId);
leftPosition = NpadController.GetJoystickPosition(leftAxisX, leftAxisY,
config.DeadzoneLeft, config.RangeLeft);
viewModel.LeftStickPosition = $"{leftPosition.Dx}, {leftPosition.Dy}";
}
if (config.RightJoystick != StickInputId.Unbound)
{
var stickInputId = (Ryujinx.Input.StickInputId)(int)config.RightJoystick;
(float rightAxisX, float rightAxisY) = gamepad.GetStick(stickInputId);
rightposition = NpadController.GetJoystickPosition(rightAxisX, rightAxisY,
config.DeadzoneRight, config.RangeRight);
viewModel.RightStickPosition = $"{rightposition.Dx}, {rightposition.Dy}";
}
viewModel.UpdateImageCss(BuildSvgCss(gamepad, config, leftPosition, rightposition));
}
await Task.Delay(100);
}
});
}
}
}

View File

@@ -81,16 +81,25 @@
<MenuItem
Command="{Binding ToggleFullscreen}"
Header="{ext:Locale MenuBarOptionsToggleFullscreen}"
Classes="withCheckbox"
Padding="0"
Icon="{ext:Icon fa-solid fa-expand}"
InputGesture="F11">
<MenuItem.Styles>
<Style Selector="Viewbox#PART_IconPresenter">
<Setter Property="MaxHeight" Value="36" />
<Setter Property="MinHeight" Value="36" />
<Setter Property="MaxWidth" Value="36" />
<Setter Property="MinWidth" Value="36" />
</Style>
<Style Selector="ContentPresenter#PART_HeaderPresenter">
<Setter Property="Padding" Value="-10,0,0,0" />
</Style>
</MenuItem.Styles>
</MenuItem>
<MenuItem
Padding="0"
Command="{Binding ToggleStartGamesInFullscreen}"
Header="{ext:Locale MenuBarOptionsStartGamesInFullscreen}"
Classes="withCheckbox">
Header="{ext:Locale MenuBarOptionsStartGamesInFullscreen}">
<MenuItem.Icon>
<CheckBox
MinWidth="{DynamicResource CheckBoxSize}"
@@ -98,26 +107,23 @@
IsChecked="{Binding StartGamesInFullscreen, Mode=TwoWay}"
Padding="0" />
</MenuItem.Icon>
</MenuItem>
<MenuItem
Padding="0"
Command="{Binding ToggleStartGamesWithoutUI}"
Header="{ext:Locale MenuBarOptionsStartGamesWithoutUI}"
Classes="withCheckbox">
<MenuItem.Icon>
<CheckBox
MinWidth="{DynamicResource CheckBoxSize}"
MinHeight="{DynamicResource CheckBoxSize}"
IsChecked="{Binding StartGamesWithoutUI, Mode=TwoWay}"
Padding="0" />
</MenuItem.Icon>
<MenuItem.Styles>
<Style Selector="Viewbox#PART_IconPresenter">
<Setter Property="MaxHeight" Value="36" />
<Setter Property="MinHeight" Value="36" />
<Setter Property="MaxWidth" Value="36" />
<Setter Property="MinWidth" Value="36" />
</Style>
<Style Selector="ContentPresenter#PART_HeaderPresenter">
<Setter Property="Padding" Value="-10,0,0,0" />
</Style>
</MenuItem.Styles>
</MenuItem>
<MenuItem
Padding="0"
IsVisible="{Binding ShowConsoleVisible}"
Command="{Binding ToggleShowConsole}"
Header="{ext:Locale MenuBarOptionsShowConsole}"
Classes="withCheckbox">
Header="{ext:Locale MenuBarOptionsShowConsole}">
<MenuItem.Icon>
<CheckBox
MinWidth="{DynamicResource CheckBoxSize}"
@@ -125,14 +131,35 @@
IsChecked="{Binding ShowConsole, Mode=TwoWay}"
Padding="0" />
</MenuItem.Icon>
<MenuItem.Styles>
<Style Selector="Viewbox#PART_IconPresenter">
<Setter Property="MaxHeight" Value="36" />
<Setter Property="MinHeight" Value="36" />
<Setter Property="MaxWidth" Value="36" />
<Setter Property="MinWidth" Value="36" />
</Style>
<Style Selector="ContentPresenter#PART_HeaderPresenter">
<Setter Property="Padding" Value="-10,0,0,0" />
</Style>
</MenuItem.Styles>
</MenuItem>
<Separator/>
<MenuItem
Name="ChangeLanguageMenuItem"
Padding="0"
Header="{ext:Locale MenuBarOptionsChangeLanguage}"
Icon="{ext:Icon fa-solid fa-language}"
Classes="withCheckbox">
Icon="{ext:Icon fa-solid fa-language}">
<MenuItem.Styles>
<Style Selector="Viewbox#PART_IconPresenter">
<Setter Property="MaxHeight" Value="36" />
<Setter Property="MinHeight" Value="36" />
<Setter Property="MaxWidth" Value="36" />
<Setter Property="MinWidth" Value="36" />
</Style>
<Style Selector="ContentPresenter#PART_HeaderPresenter">
<Setter Property="Padding" Value="-10,0,0,0" />
</Style>
</MenuItem.Styles>
</MenuItem>
<MenuItem
Name="ToggleFileTypesMenuItem"
@@ -144,8 +171,18 @@
Padding="0"
Header="{ext:Locale MenuBarOptionsSettings}"
Icon="{ext:Icon fa-solid fa-gear}"
ToolTip.Tip="{ext:Locale OpenSettingsTooltip}"
Classes="withCheckbox">
ToolTip.Tip="{ext:Locale OpenSettingsTooltip}">
<MenuItem.Styles>
<Style Selector="Viewbox#PART_IconPresenter">
<Setter Property="MaxHeight" Value="36" />
<Setter Property="MinHeight" Value="36" />
<Setter Property="MaxWidth" Value="36" />
<Setter Property="MinWidth" Value="36" />
</Style>
<Style Selector="ContentPresenter#PART_HeaderPresenter">
<Setter Property="Padding" Value="-10,0,0,0" />
</Style>
</MenuItem.Styles>
</MenuItem>
<MenuItem
Command="{Binding ManageProfiles}"
@@ -153,15 +190,25 @@
Header="{ext:Locale MenuBarOptionsManageUserProfiles}"
Icon="{ext:Icon mdi-account}"
IsEnabled="{Binding EnableNonGameRunningControls}"
ToolTip.Tip="{ext:Locale OpenProfileManagerTooltip}"
Classes="withCheckbox">
ToolTip.Tip="{ext:Locale OpenProfileManagerTooltip}">
<MenuItem.Styles>
<Style Selector="Viewbox#PART_IconPresenter">
<Setter Property="MaxHeight" Value="36" />
<Setter Property="MinHeight" Value="36" />
<Setter Property="MaxWidth" Value="36" />
<Setter Property="MinWidth" Value="36" />
</Style>
<Style Selector="ContentPresenter#PART_HeaderPresenter">
<Setter Property="Padding" Value="-10,0,0,0" />
</Style>
</MenuItem.Styles>
</MenuItem>
</MenuItem>
<MenuItem
Name="ActionsMenuItem"
VerticalAlignment="Center"
Header="{ext:Locale MenuBarActions}"
IsVisible="{Binding !EnableNonGameRunningControls}">
IsEnabled="{Binding IsGameRunning}">
<MenuItem
Name="PauseEmulationMenuItem"
Header="{ext:Locale MenuBarOptionsPauseEmulation}"
@@ -218,21 +265,21 @@
Icon="{ext:Icon fa-solid fa-code}"
IsEnabled="{Binding IsGameRunning}" />
</MenuItem>
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarActions}" IsVisible="{Binding EnableNonGameRunningControls}">
<MenuItem Header="{ext:Locale MenuBarActionsInstallKeys}" Icon="{ext:Icon fa-solid fa-key}">
<MenuItem Command="{Binding InstallKeysFromFile}" Header="{ext:Locale MenuBarFileActionsInstallKeysFromFile}" Icon="{ext:Icon mdi-file-cog}" />
<MenuItem Command="{Binding InstallKeysFromFolder}" Header="{ext:Locale MenuBarFileActionsInstallKeysFromFolder}" Icon="{ext:Icon mdi-folder-cog}" />
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarTools}">
<MenuItem Header="{ext:Locale MenuBarToolsInstallKeys}" Icon="{ext:Icon fa-solid fa-key}" IsEnabled="{Binding EnableNonGameRunningControls}">
<MenuItem Command="{Binding InstallKeysFromFile}" Header="{ext:Locale MenuBarFileToolsInstallKeysFromFile}" Icon="{ext:Icon mdi-file-cog}" />
<MenuItem Command="{Binding InstallKeysFromFolder}" Header="{ext:Locale MenuBarFileToolsInstallKeysFromFolder}" Icon="{ext:Icon mdi-folder-cog}" />
</MenuItem>
<MenuItem Header="{ext:Locale MenuBarActionsInstallFirmware}" Icon="{ext:Icon fa-solid fa-download}">
<MenuItem Command="{Binding InstallFirmwareFromFile}" Header="{ext:Locale MenuBarActionsInstallFirmwareFromFile}" Icon="{ext:Icon mdi-file-cog}" />
<MenuItem Command="{Binding InstallFirmwareFromFolder}" Header="{ext:Locale MenuBarActionsInstallFirmwareFromDirectory}" Icon="{ext:Icon mdi-folder-cog}" />
<MenuItem Header="{ext:Locale MenuBarToolsInstallFirmware}" Icon="{ext:Icon fa-solid fa-download}" IsEnabled="{Binding EnableNonGameRunningControls}">
<MenuItem Command="{Binding InstallFirmwareFromFile}" Header="{ext:Locale MenuBarFileToolsInstallFirmwareFromFile}" Icon="{ext:Icon mdi-file-cog}" />
<MenuItem Command="{Binding InstallFirmwareFromFolder}" Header="{ext:Locale MenuBarFileToolsInstallFirmwareFromDirectory}" Icon="{ext:Icon mdi-folder-cog}" />
</MenuItem>
<MenuItem Header="{ext:Locale MenuBarActionsManageFileTypes}" IsVisible="{Binding ManageFileTypesVisible}">
<MenuItem Name="InstallFileTypesMenuItem" Header="{ext:Locale MenuBarActionsInstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered, Converter={x:Static BoolConverters.Not}}" />
<MenuItem Name="UninstallFileTypesMenuItem" Header="{ext:Locale MenuBarActionsUninstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered}" />
<MenuItem Header="{ext:Locale MenuBarToolsManageFileTypes}" IsVisible="{Binding ManageFileTypesVisible}">
<MenuItem Name="InstallFileTypesMenuItem" Header="{ext:Locale MenuBarToolsInstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered, Converter={x:Static BoolConverters.Not}}" />
<MenuItem Name="UninstallFileTypesMenuItem" Header="{ext:Locale MenuBarToolsUninstallFileTypes}" IsEnabled="{Binding AreMimeTypesRegistered}" />
</MenuItem>
<Separator />
<MenuItem Name="XciTrimmerMenuItem" Header="{ext:Locale MenuBarActionsXCITrimmer}" Icon="{ext:Icon fa-solid fa-scissors}" />
<MenuItem Name="XciTrimmerMenuItem" Header="{ext:Locale MenuBarToolsXCITrimmer}" IsEnabled="{Binding EnableNonGameRunningControls}" Icon="{ext:Icon fa-solid fa-scissors}" />
</MenuItem>
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarView}">
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarViewWindow}">
@@ -264,19 +311,19 @@
Name="FaqMenuItem"
Header="{ext:Locale MenuBarHelpFaq}"
Icon="{ext:Icon fa-github}"
CommandParameter="https://github.com/Ryubing/Ryujinx/wiki/FAQ-and-Troubleshooting"
CommandParameter="https://github.com/GreemDev/Ryujinx/wiki/FAQ-and-Troubleshooting"
ToolTip.Tip="{ext:Locale MenuBarHelpFaqTooltip}" />
<MenuItem
Name="SetupGuideMenuItem"
Header="{ext:Locale MenuBarHelpSetup}"
Icon="{ext:Icon fa-github}"
CommandParameter="https://github.com/Ryubing/Ryujinx/wiki/Ryujinx-Setup-&amp;-Configuration-Guide"
CommandParameter="https://github.com/GreemDev/Ryujinx/wiki/Ryujinx-Setup-&amp;-Configuration-Guide"
ToolTip.Tip="{ext:Locale MenuBarHelpSetupTooltip}" />
<MenuItem
Name="LdnGuideMenuItem"
Header="{ext:Locale MenuBarHelpMultiplayer}"
Icon="{ext:Icon fa-github}"
CommandParameter="https://github.com/Ryubing/Ryujinx/wiki/Multiplayer%E2%80%90(LDN%E2%80%90Local%E2%80%90Wireless)%E2%80%90Guide"
CommandParameter="https://github.com/GreemDev/Ryujinx/wiki/Multiplayer%E2%80%90(LDN%E2%80%90Local%E2%80%90Wireless)%E2%80%90Guide"
ToolTip.Tip="{ext:Locale MenuBarHelpMultiplayerTooltip}" />
</MenuItem>
</MenuItem>

View File

@@ -37,20 +37,26 @@ namespace Ryujinx.Ava.UI.Views.Main
ToggleFileTypesMenuItem.ItemsSource = GenerateToggleFileTypeItems();
ChangeLanguageMenuItem.ItemsSource = GenerateLanguageMenuItems();
MiiAppletMenuItem.Command = Commands.Create(OpenMiiApplet);
CloseRyujinxMenuItem.Command = Commands.Create(CloseWindow);
OpenSettingsMenuItem.Command = Commands.Create(OpenSettings);
PauseEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Pause());
ResumeEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Resume());
StopEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.ShowExitPrompt().OrCompleted());
CheatManagerMenuItem.Command = Commands.CreateSilentFail(OpenCheatManagerForCurrentApp);
InstallFileTypesMenuItem.Command = Commands.Create(InstallFileTypes);
UninstallFileTypesMenuItem.Command = Commands.Create(UninstallFileTypes);
XciTrimmerMenuItem.Command = Commands.Create(XCITrimmerWindow.Show);
AboutWindowMenuItem.Command = Commands.Create(AboutWindow.Show);
CompatibilityListMenuItem.Command = Commands.Create(CompatibilityList.Show);
MiiAppletMenuItem.Command = new AsyncRelayCommand(OpenMiiApplet);
CloseRyujinxMenuItem.Command = new RelayCommand(CloseWindow);
OpenSettingsMenuItem.Command = new AsyncRelayCommand(OpenSettings);
PauseEmulationMenuItem.Command = new RelayCommand(() => ViewModel.AppHost?.Pause());
ResumeEmulationMenuItem.Command = new RelayCommand(() => ViewModel.AppHost?.Resume());
StopEmulationMenuItem.Command = new AsyncRelayCommand(() => ViewModel.AppHost?.ShowExitPrompt().OrCompleted());
CheatManagerMenuItem.Command = new AsyncRelayCommand(async () =>
{
try
{
await OpenCheatManagerForCurrentApp();
} catch {}
});
InstallFileTypesMenuItem.Command = new AsyncRelayCommand(InstallFileTypes);
UninstallFileTypesMenuItem.Command = new AsyncRelayCommand(UninstallFileTypes);
XciTrimmerMenuItem.Command = new AsyncRelayCommand(() => XCITrimmerWindow.Show(ViewModel));
AboutWindowMenuItem.Command = new AsyncRelayCommand(AboutWindow.Show);
CompatibilityListMenuItem.Command = new AsyncRelayCommand(CompatibilityList.Show);
UpdateMenuItem.Command = Commands.Create(async () =>
UpdateMenuItem.Command = new AsyncRelayCommand(async () =>
{
if (Updater.CanUpdate(true))
await Updater.BeginUpdateAsync(true);
@@ -58,12 +64,12 @@ namespace Ryujinx.Ava.UI.Views.Main
FaqMenuItem.Command =
SetupGuideMenuItem.Command =
LdnGuideMenuItem.Command = Commands.Create<string>(OpenHelper.OpenUrl);
LdnGuideMenuItem.Command = new RelayCommand<string>(OpenHelper.OpenUrl);
WindowSize720PMenuItem.Command =
WindowSize1080PMenuItem.Command =
WindowSize1440PMenuItem.Command =
WindowSize2160PMenuItem.Command = Commands.Create<string>(ChangeWindowSize);
WindowSize2160PMenuItem.Command = new RelayCommand<string>(ChangeWindowSize);
}
private IEnumerable<CheckBox> GenerateToggleFileTypeItems() =>
@@ -138,14 +144,14 @@ namespace Ryujinx.Ava.UI.Views.Main
ViewModel.LoadConfigurableHotKeys();
}
public AppletMetadata MiiApplet => new(ViewModel.ContentManager, "miiEdit", 0x0100000000001009);
public static readonly AppletMetadata MiiApplet = new("miiEdit", 0x0100000000001009);
public async Task OpenMiiApplet()
{
if (!MiiApplet.CanStart(out var appData, out var nacpData))
return;
await ViewModel.LoadApplication(appData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen, nacpData);
if (MiiApplet.CanStart(ViewModel.ContentManager, out var appData, out var nacpData))
{
await ViewModel.LoadApplication(appData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen, nacpData);
}
}
public async Task OpenCheatManagerForCurrentApp()

View File

@@ -105,12 +105,6 @@
GroupName="Sort"
IsChecked="{Binding IsSortedByTitle, Mode=OneTime}"
Tag="Title" />
<RadioButton
Checked="Sort_Checked"
Content="{ext:Locale DlcManagerTableHeadingTitleIdLabel}"
GroupName="Sort"
IsChecked="{Binding IsSortedByTitleId, Mode=OneTime}"
Tag="TitleId" />
<RadioButton
Checked="Sort_Checked"
Content="{ext:Locale GameListHeaderDeveloper}"

View File

@@ -43,8 +43,10 @@
<ComboBoxItem IsEnabled="{Binding IsSoundIoEnabled}">
<TextBlock Text="{ext:Locale SettingsTabSystemAudioBackendSoundIO}" />
</ComboBoxItem>
<ComboBoxItem IsEnabled="{Binding IsSDL2Enabled}">
<TextBlock Text="{ext:Locale SettingsTabSystemAudioBackendSDL2}" />
<ComboBoxItem IsEnabled="{Binding IsSDL3Enabled}">
<TextBlock>
<TextBlock Text="{ext:Locale SettingsTabSystemAudioBackendSDL3}" />
</TextBlock>
</ComboBoxItem>
</ComboBox>
</StackPanel>

View File

@@ -29,7 +29,7 @@
<TextBlock
Foreground="{DynamicResource SecondaryTextColor}"
TextDecorations="Underline"
Text="Highly specific hacks &amp; tricks to alleviate performance issues, crashing, or freezing. Will cause issues." />
Text="Game-specific hacks &amp; tricks to alleviate performance issues or crashing. Will cause issues." />
<StackPanel
Margin="0,10,0,0"
Orientation="Horizontal"

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