mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2026-05-26 23:19:15 +00:00
Compare commits
23 Commits
Canary-1.3
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb7c1fde11 | ||
|
|
18226decf1 | ||
|
|
b2310823c9 | ||
|
|
b62c58c2fe | ||
|
|
cc67e586ec | ||
|
|
8d53b02fa8 | ||
|
|
4631e0a9e1 | ||
|
|
e477ec7149 | ||
|
|
81468c1d25 | ||
|
|
004a12005e | ||
|
|
a3eda287b5 | ||
|
|
ce340e5d0b | ||
|
|
d94b759e89 | ||
|
|
b7772462f1 | ||
|
|
3473044c6e | ||
|
|
e756ad4556 | ||
|
|
58bd19a2f3 | ||
|
|
48888bd014 | ||
|
|
d307afc454 | ||
|
|
134453e62b | ||
|
|
298b6c3959 | ||
|
|
c96433eb13 | ||
|
|
c0078088dd |
@@ -28,7 +28,7 @@ jobs:
|
||||
configuration: [Release]
|
||||
platform:
|
||||
- { name: win-x64, zip_os_name: win_x64 }
|
||||
#- { name: win-arm64, zip_os_name: win_arm64 }
|
||||
- { name: win-arm64, zip_os_name: win_arm64 }
|
||||
- { name: linux-x64, zip_os_name: linux_x64 }
|
||||
- { name: linux-arm64, zip_os_name: linux_arm64 }
|
||||
#- { name: osx-x64, zip_os_name: osx_x64 }
|
||||
|
||||
@@ -32,9 +32,9 @@ jobs:
|
||||
matrix:
|
||||
platform:
|
||||
- { name: win-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_x64 }
|
||||
#- { name: win-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_arm64 }
|
||||
- { name: linux-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_x64 }
|
||||
- { name: linux-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_arm64 }
|
||||
- { name: win-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_arm64 }
|
||||
- { name: linux-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_x64 }
|
||||
- { name: linux-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_arm64 }
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
|
||||
@@ -26,9 +26,9 @@ jobs:
|
||||
matrix:
|
||||
platform:
|
||||
- { name: win-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_x64 }
|
||||
#- { name: win-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_arm64 }
|
||||
- { name: linux-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_x64 }
|
||||
- { name: linux-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_arm64 }
|
||||
- { name: win-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: win_arm64 }
|
||||
- { name: linux-x64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_x64 }
|
||||
- { name: linux-arm64, os: ghcr.io/catthehacker/ubuntu:act-latest, zip_os_name: linux_arm64 }
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -72,6 +72,9 @@ ipch/
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
|
||||
# .NET
|
||||
.dotnet-home/
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.15" />
|
||||
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.15" />
|
||||
<PackageVersion Include="Avalonia.Markup.Xaml.Loader" Version="11.3.15" />
|
||||
<PackageVersion Include="SharpCompress" Version="0.48.0" />
|
||||
<PackageVersion Include="SharpCompress" Version="0.48.1" />
|
||||
<PackageVersion Include="Svg.Controls.Avalonia" Version="11.3.9.5" />
|
||||
<PackageVersion Include="Svg.Controls.Skia.Avalonia" Version="11.3.9.5" />
|
||||
<PackageVersion Include="Microsoft.Build.Framework" Version="17.11.4" />
|
||||
@@ -45,17 +45,18 @@
|
||||
<!--<PackageVersion Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" />-->
|
||||
<PackageVersion Include="Ryujinx.Audio.OpenAL" Version="1.25.1" />
|
||||
<PackageVersion Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" Version="6.1.4-build6" />
|
||||
<PackageVersion Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Version="1.2.0" />
|
||||
<PackageVersion Include="Ryujinx.Graphics.Vulkan.MoltenVK" Version="1.4.2-ryujinx.3" />
|
||||
<PackageVersion Include="Ryujinx.LibHac" Version="0.21.0-alpha.133" />
|
||||
<PackageVersion Include="Ryujinx.UpdateClient" Version="2.0.6" />
|
||||
<PackageVersion Include="Ryujinx.Systems.Update.Common" Version="2.0.6" />
|
||||
<PackageVersion Include="Gommon" Version="2.8.1.2" />
|
||||
<PackageVersion Include="securifybv.ShellLink" Version="0.1.0" />
|
||||
<PackageVersion Include="Sep" Version="0.13.0" />
|
||||
<PackageVersion Include="Sep" Version="0.14.1" />
|
||||
<PackageVersion Include="shaderc.net" Version="0.1.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.22.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.22.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.22.0" />
|
||||
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
|
||||
<PackageVersion Include="SkiaSharp" Version="2.88.9" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Win32" Version="2.88.9" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.macOS" Version="2.88.9" />
|
||||
|
||||
@@ -5,8 +5,8 @@ Ryubing Locales uses a custom format, which uses a file for defining the support
|
||||
Each json file holds the locales for a specific part of the emulator, e.g. the Setup Wizard locales are in `SetupWizard.json`, and each locale entry in the file includes all the supported languages in the same place.
|
||||
|
||||
## Languages
|
||||
in the `/assets/` folder you will find the `Languages.json` file, which defines all the languages supported by the emulator.
|
||||
The file includes a table of the langauge codes and their langauge names.
|
||||
In the `/assets/` folder you will find the `Languages.json` file, which defines all the languages supported by the emulator.
|
||||
The file includes a table of the language codes and their language names.
|
||||
|
||||
#Example of the format for Languages.json
|
||||
{
|
||||
@@ -19,7 +19,7 @@ The file includes a table of the langauge codes and their langauge names.
|
||||
}
|
||||
|
||||
## Locales
|
||||
in the `/assets/Locales/` folder you will find the json files, which define all the locales supported by the emulator.
|
||||
In the `/assets/Locales/` folder you will find the json files, which define all the locales supported by the emulator.
|
||||
Each json file holds locales for a specific part of the emulator in a large array of locale objects.
|
||||
Each locale is made up an ID used for lookup and a list of the languages and their matching translations.
|
||||
Any empty string or null value will automatically use the English translation instead in the emulator.
|
||||
|
||||
329
assets/Locales/Dialog_Firmware.json
Normal file
329
assets/Locales/Dialog_Firmware.json
Normal file
@@ -0,0 +1,329 @@
|
||||
{
|
||||
"Locales": [
|
||||
{
|
||||
"ID": "InstallFromFileFilePickerTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "حدد ملف XCI أو أرشيف ZIP لتثبيت البرنامج الثابت منه",
|
||||
"de_DE": "Wählen Sie eine XCI-Datei oder ein ZIP-Archiv zur Firmware-Installation aus",
|
||||
"el_GR": "Επιλέξτε ένα αρχείο XCI ή ένα αρχείο ZIP για εγκατάσταση υλικολογισμικού",
|
||||
"en_US": "Select an XCI file or a ZIP archive to install firmware from",
|
||||
"es_ES": "Selecciona un archivo XCI o un archivo ZIP para instalar el firmware",
|
||||
"fr_FR": "Sélectionnez un fichier XCI ou une archive ZIP pour installer le firmware",
|
||||
"he_IL": "בחר קובץ XCI או ארכיון ZIP להתקנת קושחה",
|
||||
"it_IT": "Seleziona un file XCI o un archivio ZIP da cui installare il firmware",
|
||||
"ja_JP": "ファームウェアをインストールする XCI ファイルまたは ZIP アーカイブを選択してください",
|
||||
"ko_KR": "펌웨어를 설치할 XCI 파일 또는 ZIP 아카이브를 선택하세요",
|
||||
"no_NO": "Velg en XCI-fil eller et ZIP-arkiv for å installere fastvare fra",
|
||||
"pl_PL": "Wybierz plik XCI lub archiwum ZIP, z którego chcesz zainstalować firmware",
|
||||
"pt_BR": "Selecione um arquivo XCI ou um arquivo ZIP para instalar o firmware",
|
||||
"ru_RU": "Выберите XCI файл или ZIP-архив для установки прошивки",
|
||||
"sv_SE": "Välj en XCI-fil eller ett ZIP-arkiv för att installera firmware från",
|
||||
"th_TH": "เลือกไฟล์ XCI หรือไฟล์ ZIP เพื่อติดตั้งเฟิร์มแวร์",
|
||||
"tr_TR": "Üretici yazılımını yüklemek için bir XCI dosyası veya ZIP arşivi seçin",
|
||||
"uk_UA": "Виберіть XCI файл або ZIP-архів для встановлення прошивки",
|
||||
"zh_CN": "选择一个用于安装固件的 XCI 文件或 ZIP 压缩包",
|
||||
"zh_TW": "選擇一個用於安裝韌體的 XCI 檔案或 ZIP 壓縮檔"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallFromFolderFilePickerTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "حدد مجلدًا لتثبيت البرنامج الثابت منه",
|
||||
"de_DE": "Wählen Sie einen Ordner aus, um die Firmware zu installieren",
|
||||
"el_GR": "Επιλέξτε έναν φάκελο για να εγκαταστήσετε το firmware από αυτόν",
|
||||
"en_US": "Select a folder to install firmware from",
|
||||
"es_ES": "Selecciona una carpeta para instalar el firmware desde ella",
|
||||
"fr_FR": "Sélectionnez un dossier pour installer le firmware à partir de celui-ci",
|
||||
"he_IL": "בחר תיקיה שממנה תותקן הקושחה",
|
||||
"it_IT": "Seleziona una cartella da cui installare il firmware",
|
||||
"ja_JP": "ファームウェアをインストールするフォルダを選択してください",
|
||||
"ko_KR": "펌웨어를 설치할 폴더를 선택하세요",
|
||||
"no_NO": "Velg en mappe å installere fastvaren fra",
|
||||
"pl_PL": "Wybierz folder, z którego chcesz zainstalować oprogramowanie układowe",
|
||||
"pt_BR": "Selecione uma pasta para instalar o firmware a partir dela",
|
||||
"ru_RU": "Выберите папку, из которой будет установлена прошивка",
|
||||
"sv_SE": "Välj en mapp att installera firmware från",
|
||||
"th_TH": "เลือกโฟลเดอร์เพื่อติดตั้งเฟิร์มแวร์จากโฟลเดอร์นั้น",
|
||||
"tr_TR": "Firmware yüklemek için bir klasör seçin",
|
||||
"uk_UA": "Виберіть папку, з якої буде встановлено прошивку",
|
||||
"zh_CN": "选择一个文件夹以从中安装固件",
|
||||
"zh_TW": "選擇一個資料夾以從中安裝韌體"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "تثبيت البرنامج الثابت {0}",
|
||||
"de_DE": "Installiere Firmware {0}",
|
||||
"el_GR": "Εγκατάσταση Firmware {0}",
|
||||
"en_US": "Install Firmware {0}",
|
||||
"es_ES": "Instalar Firmware {0}",
|
||||
"fr_FR": "Installer le Firmware {0}",
|
||||
"he_IL": "התקן קושחה {0}",
|
||||
"it_IT": "Installa firmware {0}",
|
||||
"ja_JP": "ファームウェア {0} をインストール",
|
||||
"ko_KR": "펌웨어 {0} 설치",
|
||||
"no_NO": "Installer fastvare {0}",
|
||||
"pl_PL": "Zainstaluj Firmware {0}",
|
||||
"pt_BR": "Instalar Firmware {0}",
|
||||
"ru_RU": "Установить прошивку {0}",
|
||||
"sv_SE": "Installera firmware {0}",
|
||||
"th_TH": "ติดตั้งเฟิร์มแวร์ {0}",
|
||||
"tr_TR": "Firmware {0} Yükle",
|
||||
"uk_UA": "Встановити прошивку {0}",
|
||||
"zh_CN": "安装系统固件 {0}",
|
||||
"zh_TW": "安裝韌體 {0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerMainMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "سيتم تثبيت إصدار النظام {0}.",
|
||||
"de_DE": "Systemversion {0} wird jetzt installiert.",
|
||||
"el_GR": "Θα εγκατασταθεί η έκδοση συστήματος {0}.",
|
||||
"en_US": "System version {0} will be installed.",
|
||||
"es_ES": "Se instalará la versión de sistema {0}.",
|
||||
"fr_FR": "La version {0} du système sera installée.",
|
||||
"he_IL": "גירסת המערכת {0} תותקן.",
|
||||
"it_IT": "La versione del sistema {0} sarà installata.",
|
||||
"ja_JP": "システムバージョン {0} がインストールされます。",
|
||||
"ko_KR": "시스템 버전 {0}이(가) 설치됩니다.",
|
||||
"no_NO": "Systemversjon {0} vil bli installert.",
|
||||
"pl_PL": "Wersja systemu {0} zostanie zainstalowana.",
|
||||
"pt_BR": "A versão do sistema {0} será instalada.",
|
||||
"ru_RU": "Будет установлена версия прошивки {0}.",
|
||||
"sv_SE": "Systemversion {0} kommer att installeras.",
|
||||
"th_TH": "ระบบเวอร์ชั่น {0} ได้รับการติดตั้งเร็วๆ นี้",
|
||||
"tr_TR": "Sistem sürümü {0} yüklenecek.",
|
||||
"uk_UA": "Буде встановлено версію системи {0}.",
|
||||
"zh_CN": "即将安装系统固件版本 {0} 。",
|
||||
"zh_TW": "即將安裝系統韌體版本 {0}。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerSubMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "\n\nهذا سيحل محل إصدار النظام الحالي {0}.",
|
||||
"de_DE": "\n\nDies wird die aktuelle Systemversion {0} ersetzen.",
|
||||
"el_GR": "\n\nΑυτό θα αντικαταστήσει την τρέχουσα έκδοση συστήματος {0}.",
|
||||
"en_US": "\n\nThis will replace the current system version {0}.",
|
||||
"es_ES": "\n\nEsto reemplazará la versión de sistema actual, {0}.",
|
||||
"fr_FR": "\n\nCela remplacera la version actuelle du système {0}.",
|
||||
"he_IL": "\n\nזה יחליף את גרסת המערכת הנוכחית {0}.",
|
||||
"it_IT": "\n\nQuesta sostituirà l'attuale versione del sistema ({0}).",
|
||||
"ja_JP": "\n\n現在のシステムバージョン {0} を置き換えます。",
|
||||
"ko_KR": "\n\n현재 시스템 버전 {0}을(를) 대체합니다.",
|
||||
"no_NO": "\n\nDette erstatter den gjeldende systemversjonen {0}.",
|
||||
"pl_PL": "\n\nZastąpi to obecną wersję systemu {0}.",
|
||||
"pt_BR": "\n\nIsso substituirá a versão do sistema atual {0}.",
|
||||
"ru_RU": "\n\nЭто заменит текущую версию прошивки {0}.",
|
||||
"sv_SE": "\n\nDetta kommer att ersätta aktuella systemversionen {0}.",
|
||||
"th_TH": "\n\nสิ่งนี้จะแทนที่เวอร์ชั่นของระบบเวอร์ชั่นปัจจุบัน {0}.",
|
||||
"tr_TR": "\n\nBu şimdiki sistem sürümünün yerini alacak {0}.",
|
||||
"uk_UA": "\n\nЦе замінить поточну версію системи {0}.",
|
||||
"zh_CN": "\n\n替换当前系统固件版本 {0} 。",
|
||||
"zh_TW": "\n\n這將取代目前的系統韌體版本 {0}。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerConfirmMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "\nهل تريد المتابعة؟",
|
||||
"de_DE": "\n\nMöchtest du fortfahren?",
|
||||
"el_GR": "\n\nΘέλετε να συνεχίσετε;",
|
||||
"en_US": "\n\nDo you want to continue?",
|
||||
"es_ES": "\n\n¿Continuar?",
|
||||
"fr_FR": "\n\nVoulez-vous continuer ?",
|
||||
"he_IL": "\n\nהאם ברצונך להמשיך?",
|
||||
"it_IT": "\n\nVuoi continuare?",
|
||||
"ja_JP": "\n\n続けてよろしいですか?",
|
||||
"ko_KR": "\n\n계속하시겠습니까?",
|
||||
"no_NO": "\n\nVil du fortsette?",
|
||||
"pl_PL": "\n\nCzy chcesz kontynuować?",
|
||||
"pt_BR": "\n\nDeseja continuar?",
|
||||
"ru_RU": "\n\nПродолжить?",
|
||||
"sv_SE": "\n\nVill du fortsätta?",
|
||||
"th_TH": "\n\nคุณต้องการดำเนินการต่อหรือไม่?",
|
||||
"tr_TR": "\n\nDevam etmek istiyor musunuz?",
|
||||
"uk_UA": "\n\nВи хочете продовжити?",
|
||||
"zh_CN": "\n\n是否继续?",
|
||||
"zh_TW": "\n\n您確定要繼續嗎?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerWaitMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "تثبيت البرنامج الثابت...",
|
||||
"de_DE": "Firmware wird installiert...",
|
||||
"el_GR": "Εγκατάσταση Firmware...",
|
||||
"en_US": "Installing Firmware...",
|
||||
"es_ES": "Instalando Firmware...",
|
||||
"fr_FR": "Installation du Firmware...",
|
||||
"he_IL": "מתקין קושחה...",
|
||||
"it_IT": "Installazione del firmware...",
|
||||
"ja_JP": "ファームウェアをインストール中...",
|
||||
"ko_KR": "펌웨어 설치 중...",
|
||||
"no_NO": "Installerer fastvare...",
|
||||
"pl_PL": "Instalowanie firmware'u...",
|
||||
"pt_BR": "Instalando firmware...",
|
||||
"ru_RU": "Установка прошивки...",
|
||||
"sv_SE": "Installerar firmware...",
|
||||
"th_TH": "กำลังติดตั้งเฟิร์มแวร์...",
|
||||
"tr_TR": "Firmware yükleniyor...",
|
||||
"uk_UA": "Встановлення прошивки...",
|
||||
"zh_CN": "安装系统固件中...",
|
||||
"zh_TW": "正在安裝韌體..."
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerSuccessMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "تم تثبيت إصدار النظام {0} بنجاح.",
|
||||
"de_DE": "Systemversion {0} wurde erfolgreich installiert.",
|
||||
"el_GR": "Η έκδοση συστήματος {0} εγκαταστάθηκε με επιτυχία.",
|
||||
"en_US": "System version {0} successfully installed.",
|
||||
"es_ES": "Versión de sistema {0} instalada con éxito.",
|
||||
"fr_FR": "Version du système {0} installée avec succès.",
|
||||
"he_IL": "גרסת המערכת {0} הותקנה בהצלחה.",
|
||||
"it_IT": "La versione del sistema {0} è stata installata.",
|
||||
"ja_JP": "システムバージョン {0} が正常にインストールされました。",
|
||||
"ko_KR": "시스템 버전 {0}이(가) 설치되었습니다.",
|
||||
"no_NO": "Systemversjon {0} ble installert.",
|
||||
"pl_PL": "Wersja systemu {0} została pomyślnie zainstalowana.",
|
||||
"pt_BR": "Versão do sistema {0} instalada com sucesso.",
|
||||
"ru_RU": "Прошивка версии {0} успешно установлена.",
|
||||
"sv_SE": "Systemversion {0} har installerats.",
|
||||
"th_TH": "ระบบเวอร์ชั่น {0} ติดตั้งเรียบร้อยแล้ว",
|
||||
"tr_TR": "Sistem sürümü {0} başarıyla yüklendi.",
|
||||
"uk_UA": "Версію системи {0} успішно встановлено.",
|
||||
"zh_CN": "成功安装系统固件版本 {0}。",
|
||||
"zh_TW": "成功安裝系統韌體版本 {0}。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerInstalledMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "تم تثبيت البرنامج الثابت {0}",
|
||||
"de_DE": "Firmware {0} wurde installiert",
|
||||
"el_GR": "Το Firmware {0} εγκαταστάθηκε",
|
||||
"en_US": "Firmware {0} was installed",
|
||||
"es_ES": "Se Instaló el Firmware {0}",
|
||||
"fr_FR": "Le firmware {0} a été installé",
|
||||
"he_IL": "הקושחה {0} הותקנה",
|
||||
"it_IT": "Il firmware {0} è stato installato",
|
||||
"ja_JP": "ファームウェア {0} がインストールされました",
|
||||
"ko_KR": "펌웨어 {0}이(가) 설치됨",
|
||||
"no_NO": "fastvare {0} ble installert",
|
||||
"pl_PL": "Firmware {0} został zainstalowany",
|
||||
"pt_BR": "Firmware {0} foi instalado",
|
||||
"ru_RU": "Прошивка {0} была установлена",
|
||||
"sv_SE": "Firmware {0} installerades",
|
||||
"th_TH": "เฟิร์มแวร์ {0} ติดตั้งแล้ว",
|
||||
"tr_TR": "Yazılım {0} yüklendi",
|
||||
"uk_UA": "Встановлено прошивку {0}",
|
||||
"zh_CN": "已安装系统固件 {0}",
|
||||
"zh_TW": "已安裝韌體{0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerNotInstalledMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "لا يوجد برنامج ثابت مثبت",
|
||||
"de_DE": "Keine Firmware installiert.",
|
||||
"el_GR": "Δεν έχει εγκατασταθεί Firmware.",
|
||||
"en_US": "No Firmware Installed.",
|
||||
"es_ES": "No hay Firmware Instalado.",
|
||||
"fr_FR": "Aucun Firmware Installé.",
|
||||
"he_IL": "לא מותקנת קושחה.",
|
||||
"it_IT": "Nessun firmware installato.",
|
||||
"ja_JP": "ファームウェアがインストールされていません。",
|
||||
"ko_KR": "펌웨어가 설치되어 있지 .않음",
|
||||
"no_NO": "Ingen fastvare installert.",
|
||||
"pl_PL": "Brak Zainstalowanego Firmware'u.",
|
||||
"pt_BR": "Nenhum Firmware Instalado.",
|
||||
"ru_RU": "Прошивка не установлена.",
|
||||
"sv_SE": "Inget firmware installerat.",
|
||||
"th_TH": "ไม่มีการติดตั้งเฟิร์มแวร์",
|
||||
"tr_TR": "Yazılım Yüklü Değil.",
|
||||
"uk_UA": "Прошивка не встановлена.",
|
||||
"zh_CN": "未安装系统固件。",
|
||||
"zh_TW": "未安裝韌體。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerFirmwareNotFound",
|
||||
"Translations": {
|
||||
"ar_SA": "لم يتم العثور على برنامج ثابت للنظام صالح في {0}.",
|
||||
"de_DE": "Es wurde keine gültige System-Firmware gefunden in {0}.",
|
||||
"el_GR": "Δεν βρέθηκε έγκυρο Firmware συστήματος στο {0}.",
|
||||
"en_US": "A valid system firmware was not found in {0}.",
|
||||
"es_ES": "No se pudo encontrar un firmware válido en {0}.",
|
||||
"fr_FR": "Un firmware valide n'a pas été trouvé dans {0}.",
|
||||
"he_IL": "לא נמצאה קושחת מערכת תקפה ב-{0}.",
|
||||
"it_IT": "Un firmware del sistema valido non è stato trovato in {0}.",
|
||||
"ja_JP": "{0} には有効なシステムファームウェアがありません。",
|
||||
"ko_KR": "{0}에서 유효한 시스템 펌웨어를 찾을 수 없습니다.",
|
||||
"no_NO": "En gyldig systemfastvare ble ikke funnet i {0}.",
|
||||
"pl_PL": "Nie znaleziono prawidłowego firmware'u systemowego w {0}.",
|
||||
"pt_BR": "Um firmware de sistema válido não foi encontrado em {0}.",
|
||||
"ru_RU": "Не удалось найти действительную системную прошивку в {0}.",
|
||||
"sv_SE": "Ett giltigt systemfirmware hittades inte i {0}.",
|
||||
"th_TH": "ไม่พบเฟิร์มแวร์ของระบบที่ถูกต้อง {0}.",
|
||||
"tr_TR": "{0} da geçerli bir sistem firmware'i bulunamadı.",
|
||||
"uk_UA": "Дійсна прошивка системи не знайдена в {0}.",
|
||||
"zh_CN": "在路径 {0} 中找不到有效的 Switch 系统固件。",
|
||||
"zh_TW": "在 {0} 中未發現有效的系統韌體。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerEmbeddedMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "هل ترغب في تثبيت البرنامج الثابت المدمج في هذه اللعبة؟ (البرنامج الثابت {0})",
|
||||
"de_DE": "Die in diesem Spiel enthaltene Firmware installieren? (Firmware {0})",
|
||||
"el_GR": "Θα θέλατε να εγκαταστήσετε το Firmware που είναι ενσωματωμένο σε αυτό το παιχνίδι; (Firmware {0})",
|
||||
"en_US": "Would you like to install the firmware embedded in this game? (Firmware {0})",
|
||||
"es_ES": "¿Quieres instalar el firmware incluido en este juego? (Firmware versión {0})",
|
||||
"fr_FR": "Voulez-vous installer le firmware intégré dans ce jeu ? (Firmware {0})",
|
||||
"he_IL": "האם תרצו להתקין את הקושחה המוטמעת במשחק הזה? (קושחה {0})",
|
||||
"it_IT": "Vuoi installare il firmware incluso in questo gioco? (Firmware {0})",
|
||||
"ja_JP": "このゲームに含まれるファームウェアをインストールしてよろしいですか? (ファームウェア {0})",
|
||||
"ko_KR": "이 게임에 포함된 펌웨어를 설치하시겠습니까?(Firmware {0})",
|
||||
"no_NO": "Ønsker du å installere fastvaren innebygd i dette spillet? (Firmware {0})",
|
||||
"pl_PL": "Czy chcesz zainstalować firmware wbudowany w tę grę? (Firmware {0})",
|
||||
"pt_BR": "Gostaria de instalar o firmware incluso neste jogo? (Firmware {0})",
|
||||
"ru_RU": "Хотите установить прошивку, встроенную в эту игру? (Прошивка {0})",
|
||||
"sv_SE": "Vill du installera det firmware som är inbäddat i detta spel? (Firmware {0})",
|
||||
"th_TH": "คุณต้องการติดตั้งเฟิร์มแวร์ที่ฝังอยู่ในเกมนี้หรือไม่? (เฟิร์มแวร์ {0})",
|
||||
"tr_TR": "Bu oyunun içine gömülü olan yazılımı yüklemek ister misiniz? (Firmware {0})",
|
||||
"uk_UA": "Бажаєте встановити прошивку, вбудовану в цю гру? (Прошивка {0})",
|
||||
"zh_CN": "要安装游戏文件中内嵌的系统固件吗?(固件版本 {0})",
|
||||
"zh_TW": "您想安裝遊戲內建的韌體嗎? (韌體 {0})"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerEmbeddedSuccessMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "لم يتم العثور على أي برنامج ثابت مثبت ولكن ريوجينكس كان قادرا على تثبيت البرنامج الثابت {0} من اللعبة المقدمة.\nسيبدأ المحاكي الآن.",
|
||||
"de_DE": "Es wurde keine installierte Firmware gefunden, aber Ryujinx konnte die Firmware {0} aus dem bereitgestellten Spiel installieren.\nRyujinx wird nun gestartet.",
|
||||
"el_GR": "Δεν βρέθηκε εγκατεστημένο υλικολογισμικό, αλλά το Ryujinx κατάφερε να εγκαταστήσει το υλικολογισμικό {0} από το παρεχόμενο παιχνίδι.\nΟ προσομοιωτής θα ξεκινήσει τώρα.",
|
||||
"en_US": "No installed firmware was found but Ryujinx was able to install firmware {0} from the provided game.\nThe emulator will now start.",
|
||||
"es_ES": "No se encontró ningún firmware instalado, pero Ryujinx pudo instalar el firmware {0} del juego proporcionado.\nEl emulador iniciará.",
|
||||
"fr_FR": "Aucun firmware installé n'a été trouvé mais Ryujinx a pu installer le firmware {0} à partir du jeu fourni.\nL'émulateur va maintenant démarrer.",
|
||||
"he_IL": "לא נמצאה קושחה מותקנת אבל ריוג'ינקס הצליח להתקין קושחה {0} מהמשחק שסופק. \nהאמולטור יופעל כעת.",
|
||||
"it_IT": "Non è stato trovato alcun firmware installato, ma Ryujinx è riuscito ad installare il firmware {0} dal gioco fornito.\nL'emulatore si avvierà adesso.",
|
||||
"ja_JP": "ファームウェアがインストールされていませんが, ゲームに含まれるファームウェア {0} をインストールできます.\nエミュレータが開始します。",
|
||||
"ko_KR": "설치된 펌웨어를 찾을 수 없지만 Ryujinx는 제공된 게임에서 펌웨어 {0}을(를) 설치할 수 있습니다.\n이제 에뮬레이터가 시작됩니다.",
|
||||
"no_NO": "Det ble ikke funnet noen installert fastvare, men Ryujinx kunne installere fastvare {0} fra det oppgitte spillet.\nemulatoren vil nå starte.",
|
||||
"pl_PL": "Nie znaleziono zainstalowanego oprogramowania, ale Ryujinx był w stanie zainstalować oprogramowanie {0} z dostarczonej gry.\n\nEmulator uruchomi się teraz.",
|
||||
"pt_BR": "Nenhum firmware instalado foi encontrado, mas o Ryujinx conseguiu instalar o firmware {0} a partir do jogo fornecido.\nO emulador será iniciado agora.",
|
||||
"ru_RU": "Установленной прошивки не было найдено, но Ryujinx удалось установить прошивку {0} из предоставленной игры.\nТеперь запустится эмулятор.",
|
||||
"sv_SE": "Inget installerat firmware hittades men Ryujinx kunde installera firmware {0} från angiven spel.\nEmulatorn kommer nu att startas.",
|
||||
"th_TH": "ไม่พบเฟิร์มแวร์ที่ติดตั้งไว้ แต่ Ryujinx จะติดตั้งเฟิร์มแวร์ได้ {0} จากเกมที่ให้มา\nขณะนี้โปรแกรมจำลองจะเริ่มทำงาน",
|
||||
"tr_TR": "Yüklü bir firmware bulunamadı, ancak Ryujinx sağlanan oyundan firmware {0} yüklemeyi başardı.\nEmülatör şimdi başlatılacak.",
|
||||
"uk_UA": "Встановлену прошивку не знайдено, але Ryujinx вдалося встановити прошивку {0} з наданої гри.\nТепер запуститься емулятор.",
|
||||
"zh_CN": "Ryujinx 模拟器已经从当前游戏文件中安装了系统固件 {0} 。\n模拟器现在可以正常运行了。",
|
||||
"zh_TW": "未找到已安裝的韌體,但 Ryujinx 可以從現有的遊戲安裝韌體{0}。\n模擬器現在可以執行。"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
204
assets/Locales/Dialog_Keys.json
Normal file
204
assets/Locales/Dialog_Keys.json
Normal file
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"Locales": [
|
||||
{
|
||||
"ID": "InstallFromFileFilePickerTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "حدد ملف KEYS لتثبيت المفاتيح منه",
|
||||
"de_DE": "Wählen Sie eine KEYS-Datei zur Schlüsselinstallation aus",
|
||||
"el_GR": "Επιλέξτε ένα αρχείο KEYS για εγκατάσταση κλειδιών",
|
||||
"en_US": "Select a KEYS file to install keys from",
|
||||
"es_ES": "Selecciona un archivo KEYS para instalar las claves",
|
||||
"fr_FR": "Sélectionnez un fichier KEYS pour installer les clés",
|
||||
"he_IL": "בחר קובץ KEYS להתקנת מפתחות",
|
||||
"it_IT": "Seleziona un file KEYS da cui installare le chiavi",
|
||||
"ja_JP": "キーをインストールする KEYS ファイルを選択してください",
|
||||
"ko_KR": "키를 설치할 KEYS 파일을 선택하세요",
|
||||
"no_NO": "Velg en KEYS-fil for å installere nøkler fra",
|
||||
"pl_PL": "Wybierz plik KEYS, z którego chcesz zainstalować klucze",
|
||||
"pt_BR": "Selecione um arquivo KEYS para instalar as chaves",
|
||||
"ru_RU": "Выберите KEYS файл для установки ключей",
|
||||
"sv_SE": "Välj en KEYS-fil för att installera nycklar från",
|
||||
"th_TH": "เลือกไฟล์ KEYS เพื่อติดตั้งคีย์",
|
||||
"tr_TR": "Anahtarları yüklemek için bir KEYS dosyası seçin",
|
||||
"uk_UA": "Виберіть KEYS файл для встановлення ключів",
|
||||
"zh_CN": "选择一个用于安装密钥的 KEYS 文件",
|
||||
"zh_TW": "選擇一個用於安裝金鑰的 KEYS 檔案"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallFromFolderFilePickerTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "حدد مجلدًا لتثبيت المفاتيح منه",
|
||||
"de_DE": "Wählen Sie einen Ordner aus, um die Schlüssel zu installieren",
|
||||
"el_GR": "Επιλέξτε έναν φάκελο για να εγκαταστήσετε τα κλειδιά",
|
||||
"en_US": "Select a folder to install keys from",
|
||||
"es_ES": "Selecciona una carpeta para instalar las claves",
|
||||
"fr_FR": "Sélectionnez un dossier pour installer les clés",
|
||||
"he_IL": "בחר תיקיה להתקין ממנה את המפתחות",
|
||||
"it_IT": "Seleziona una cartella da cui installare le chiavi",
|
||||
"ja_JP": "キーをインストールするフォルダを選択してください",
|
||||
"ko_KR": "키를 설치할 폴더를 선택하세요",
|
||||
"no_NO": "Velg en mappe å installere nøklene fra",
|
||||
"pl_PL": "Wybierz folder, z którego zainstalować klucze",
|
||||
"pt_BR": "Selecione uma pasta para instalar as chaves",
|
||||
"ru_RU": "Выберите папку, из которой установить ключи",
|
||||
"sv_SE": "Välj en mapp att installera nycklar från",
|
||||
"th_TH": "เลือกโฟลเดอร์เพื่อติดตั้งคีย์",
|
||||
"tr_TR": "Anahtarları yüklemek için bir klasör seçin",
|
||||
"uk_UA": "Виберіть папку, з якої встановити ключі",
|
||||
"zh_CN": "选择一个文件夹以安装密钥",
|
||||
"zh_TW": "選擇一個資料夾以安裝密鑰"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerMainMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "سيتم تثبيت ملف مفاتيح جديد.",
|
||||
"de_DE": "Eine neue Schlüsseldatei wird installiert.",
|
||||
"el_GR": "Ένα νέο αρχείο Κλειδιών θα εγκατασταθεί.",
|
||||
"en_US": "New Keys file will be installed.",
|
||||
"es_ES": "Un nuevo archivo de Claves será instalado.",
|
||||
"fr_FR": "Nouveau fichier de Clés sera installé.",
|
||||
"he_IL": "קובץ מפתחות חדש יותקן.",
|
||||
"it_IT": "Un nuovo file di chiavi sarà installato.",
|
||||
"ja_JP": "新しいキー ファイルがインストールされます。",
|
||||
"ko_KR": "새로운 키 파일이 설치됩니다.",
|
||||
"no_NO": "Ny Keys-fil vil bli installert.",
|
||||
"pl_PL": "Nowy plik kluczy zostanie zainstalowany.",
|
||||
"pt_BR": "O novo arquivo Chaves será instalado.",
|
||||
"ru_RU": "Будут установлены новые ключи.",
|
||||
"sv_SE": "Ny nyckelfil kommer att installeras.",
|
||||
"th_TH": "กำลังติดตั้งไฟล์ Keys ใหม่",
|
||||
"tr_TR": "Yeni anahtar dosyası yüklenecek.",
|
||||
"uk_UA": "Новий файл Ключів буде встановлено.",
|
||||
"zh_CN": "将会安装新密匙文件。",
|
||||
"zh_TW": "將會安裝新增的金鑰檔案。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerSubMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "\n\nقد يحل هذا محل بعض المفاتيح المثبتة حاليًا.",
|
||||
"de_DE": "\n\nDies könnte einige der derzeit installierten Schlüssel ersetzen.",
|
||||
"el_GR": "\n\nΑυτό μπορεί να αντικαταστήσει μερικά από τα τρέχοντα εγκατεστημένα κλειδιά.",
|
||||
"en_US": "\n\nThis may replace some of the current installed Keys.",
|
||||
"es_ES": "\n\nEsto puede reemplazar algunas de las Keys actualmente instaladas.",
|
||||
"fr_FR": "\n\nCela peut remplacer certaines des Clés actuellement installées.",
|
||||
"he_IL": "\n\nזה עשוי להחליף חלק מהמפתחות המותקנים הנוכחיים.",
|
||||
"it_IT": "\n\nAlcune delle chiavi già installate potrebbero essere sovrascritte.",
|
||||
"ja_JP": "\n\nこれにより、現在インストールされているキーの一部が置き換えられる場合があります。",
|
||||
"ko_KR": "\n\n이로 인해 현재 설치된 키 중 일부가 대체될 수 있습니다.",
|
||||
"no_NO": "\n\nDette kan erstatte noen av de nåværende installerte nøklene.",
|
||||
"pl_PL": "\n\nTo może zastąpić niektóre z aktualnie zainstalowanych kluczy.",
|
||||
"pt_BR": "\n\nIsso pode substituir algumas das chaves instaladas atualmente.",
|
||||
"ru_RU": "\n\nЭто может заменить некоторые из текущих установленных ключей.",
|
||||
"sv_SE": "\n\nDetta kan ersätta några av de redan installerade nycklarna.",
|
||||
"th_TH": "\n\nสิ่งนี้อาจทำให้ไฟล์ Keys บางส่วนที่ติดตั้งอยู่ถูกแทนที่",
|
||||
"tr_TR": "\n\nBu, şu anda kurulu olan anahtarların bazılarının yerine geçebilir.",
|
||||
"uk_UA": "\n\nЦе замінить собою поточні файли Ключів.",
|
||||
"zh_CN": "\n\n这也许会替换掉一些当前已安装的密匙。",
|
||||
"zh_TW": "\n\n這將取代部分已安裝的金鑰。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerConfirmInstall",
|
||||
"Translations": {
|
||||
"ar_SA": "\nهل تريد المتابعة؟",
|
||||
"de_DE": "\n\nMöchtest du fortfahren?",
|
||||
"el_GR": "\n\nΘέλετε να συνεχίσετε;",
|
||||
"en_US": "\n\nDo you want to continue?",
|
||||
"es_ES": "\n\n¿Continuar?",
|
||||
"fr_FR": "\n\nVoulez-vous continuer ?",
|
||||
"he_IL": "\n\nהאם ברצונך להמשיך?",
|
||||
"it_IT": "\n\nVuoi continuare?",
|
||||
"ja_JP": "\n\n続けてよろしいですか?",
|
||||
"ko_KR": "\n\n계속하시겠습니까?",
|
||||
"no_NO": "\n\nVil du fortsette?",
|
||||
"pl_PL": "\n\nCzy chcesz kontynuować?",
|
||||
"pt_BR": "\n\nDeseja continuar?",
|
||||
"ru_RU": "\n\nПродолжить?",
|
||||
"sv_SE": "\n\nVill du fortsätta?",
|
||||
"th_TH": "\n\nคุณต้องการดำเนินการต่อหรือไม่?",
|
||||
"tr_TR": "\n\nDevam etmek istiyor musunuz?",
|
||||
"uk_UA": "\n\nВи хочете продовжити?",
|
||||
"zh_CN": "\n\n是否继续?",
|
||||
"zh_TW": "\n\n您確定要繼續嗎?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerWaitMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "جارٍ تثبيت المفاتيح...",
|
||||
"de_DE": "Schlüssel werden installiert...",
|
||||
"el_GR": "Εγκατάσταση κλειδιών...",
|
||||
"en_US": "Installing Keys...",
|
||||
"es_ES": "Instalando Claves...",
|
||||
"fr_FR": "Installation des Clés...",
|
||||
"he_IL": "מתקין מפתחות...",
|
||||
"it_IT": "Installazione delle chiavi...",
|
||||
"ja_JP": "キーをインストールしています...",
|
||||
"ko_KR": "키 설치 중...",
|
||||
"no_NO": "Installere nøkler...",
|
||||
"pl_PL": "Instalowanie kluczy...",
|
||||
"pt_BR": "Instalando Chaves...",
|
||||
"ru_RU": "Установка ключей...",
|
||||
"sv_SE": "Installerar nycklar...",
|
||||
"th_TH": "กำลังดำเนินการติดตั้ง Keys...",
|
||||
"tr_TR": "Anahtarlar yükleniyor...",
|
||||
"uk_UA": "Встановлення Ключів...",
|
||||
"zh_CN": "安装密匙中。。。",
|
||||
"zh_TW": "正在安裝金鑰。。。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerSuccessMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "تم تثبيت ملف المفاتيح الجديد بنجاح.",
|
||||
"de_DE": "Neue Schlüsseldatei erfolgreich installiert.",
|
||||
"el_GR": "Το νέο αρχείο Κλειδιών εγκαταστάθηκε με επιτυχία.",
|
||||
"en_US": "New Keys file successfully installed.",
|
||||
"es_ES": "Nuevo archivo Keys instalado con éxito.",
|
||||
"fr_FR": "Nouveau fichier de Clés a été installé.",
|
||||
"he_IL": "הקובץ החדש של המפתחות הותקן בהצלחה.",
|
||||
"it_IT": "Nuovo file di chiavi installato con successo.",
|
||||
"ja_JP": "新しいキー ファイルが正常にインストールされました。",
|
||||
"ko_KR": "새로운 키 파일이 성공적으로 설치되었습니다.",
|
||||
"no_NO": "Ny Keys -fil installert.",
|
||||
"pl_PL": "Nowy plik kluczy został pomyślnie zainstalowany.",
|
||||
"pt_BR": "Novo arquivo de chaves instalado com sucesso.",
|
||||
"ru_RU": "Новые ключи успешно установлены.",
|
||||
"sv_SE": "Ny nyckelfil installerades.",
|
||||
"th_TH": "การติดตั้งไฟล์ Keys ใหม่เสร็จสมบูรณ์แล้ว",
|
||||
"tr_TR": "Yeni anahtar dosyası başarıyla yüklendi.",
|
||||
"uk_UA": "Нові ключі встановлено.",
|
||||
"zh_CN": "已成功安装新密匙文件。",
|
||||
"zh_TW": "成功安裝新增的金鑰檔案。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallerInvalidKeysFoundMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "An invalid Keys file was found in {0}.",
|
||||
"es_ES": "Se halló un archivo Keys inválido en {0}.",
|
||||
"fr_FR": "Un fichier de Clés invalide a été trouvé dans {0}.",
|
||||
"he_IL": "",
|
||||
"it_IT": "È stato trovato un file di chiavi non valido in {0}.",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "{0}에서 잘못된 키 파일이 발견.",
|
||||
"no_NO": "En ugyldig Keys-fil ble funnet i {0}.",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "Um arquivo Chaves inválido foi encontrado em {0}.",
|
||||
"ru_RU": "В {0} найден некорректный файл ключей.",
|
||||
"sv_SE": "En ogiltig nyckelfil hittades i {0}.",
|
||||
"th_TH": "พบไฟล์ Keys ที่ไม่ถูกต้องใน {0}.",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "Виявлено неправильний файл ключів у теці {0}.",
|
||||
"zh_CN": "在 {0} 发现了一个无效的密匙文件。",
|
||||
"zh_TW": "找到無效的金鑰檔案 {0}。"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
154
assets/Locales/Error.json
Normal file
154
assets/Locales/Error.json
Normal file
@@ -0,0 +1,154 @@
|
||||
{
|
||||
"Locales": [
|
||||
{
|
||||
"ID": "NoKeysFound",
|
||||
"Translations": {
|
||||
"ar_SA": "المفاتيح غير موجودة.",
|
||||
"de_DE": "Keys nicht gefunden.",
|
||||
"el_GR": "Τα κλειδιά δεν βρέθηκαν.",
|
||||
"en_US": "Keys not found.",
|
||||
"es_ES": "No se encontraron claves.",
|
||||
"fr_FR": "Clés non trouvées.",
|
||||
"he_IL": "המפתחות לא נמצאו.",
|
||||
"it_IT": "Chiavi non trovate.",
|
||||
"ja_JP": "Keys がありません。",
|
||||
"ko_KR": "키를 찾을 수 없음.",
|
||||
"no_NO": "Finner ikke nøkler.",
|
||||
"pl_PL": "Nie znaleziono kluczy.",
|
||||
"pt_BR": "Chaves não encontradas.",
|
||||
"ru_RU": "Ключи не найдены.",
|
||||
"sv_SE": "Nycklarna hittades inte.",
|
||||
"th_TH": "ไม่พบ คีย์",
|
||||
"tr_TR": "Keys bulunamadı.",
|
||||
"uk_UA": "Ключі не знайдено.",
|
||||
"zh_CN": "找不到密钥。",
|
||||
"zh_TW": "找不到金鑰。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "NoKeysFoundDescription",
|
||||
"Translations": {
|
||||
"ar_SA": "لم يتمكن ريوجينكس من العثور على ملف \"prod.keys\" الخاص بك.",
|
||||
"de_DE": "Ryujinx konnte deine \"prod.keys\" Datei nicht finden.",
|
||||
"el_GR": "Το Ryujinx δεν κατάφερε να εντοπίσει το αρχείο \"prod.keys\".",
|
||||
"en_US": "Ryujinx was unable to find your \"prod.keys\" file.",
|
||||
"es_ES": "Ryujinx no pudo encontrar tus \"prod.keys\".",
|
||||
"fr_FR": "Ryujinx n'a pas pu trouver votre fichier \"prod.keys\".",
|
||||
"he_IL": "ריוג'ינקס לא הצליח למצוא את קובץ ה-\"prod.keys\" שלך.",
|
||||
"it_IT": "Ryujinx non è riuscito a trovare il file \"prod.keys\".",
|
||||
"ja_JP": "\"prod.keys\" が見つかりませんでした。",
|
||||
"ko_KR": "Ryujinx가 '\"prod.keys\" 파일을 찾지 못함.",
|
||||
"no_NO": "Ryujinx kunne ikke finne \"prod.keys\" filen din.",
|
||||
"pl_PL": "Ryujinx nie mógł znaleźć twojego pliku \"prod.keys\".",
|
||||
"pt_BR": "Ryujinx não conseguiu encontrar o seu arquivo '\"prod.keys\".",
|
||||
"ru_RU": "Ryujinx не удалось найти ваш \"prod.keys\" файл.",
|
||||
"sv_SE": "Ryujinx kunde inte hitta din \"prod.keys\"-fil.",
|
||||
"th_TH": "Ryujinx ไม่พบไฟล์ '\"prod.keys\" ในเครื่องของคุณ",
|
||||
"tr_TR": "Ryujinx \"prod.keys\" dosyasını bulamadı.",
|
||||
"uk_UA": "Ryujinx не вдалося знайти ваш файл \"prod.keys\".",
|
||||
"zh_CN": "Ryujinx 模拟器找不到“prod.keys”密钥文件。",
|
||||
"zh_TW": "Ryujinx 無法找到您的「prod.keys」檔案。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "NoFirmwareFound",
|
||||
"Translations": {
|
||||
"ar_SA": "لم يتم العثور على البرنامج الثابت",
|
||||
"de_DE": "Firmware nicht gefunden",
|
||||
"el_GR": "Το firmware δε βρέθηκε",
|
||||
"en_US": "Firmware not found",
|
||||
"es_ES": "No se encontró Firmware",
|
||||
"fr_FR": "Firmware introuvable",
|
||||
"he_IL": "קושחה לא נמצאה",
|
||||
"it_IT": "Firmware non trovato",
|
||||
"ja_JP": "ファームウェアがありません",
|
||||
"ko_KR": "펌웨어를 찾을 수 없음",
|
||||
"no_NO": "Fastvare ikke funnet",
|
||||
"pl_PL": "Nie znaleziono firmware'u",
|
||||
"pt_BR": "Firmware não encontrado",
|
||||
"ru_RU": "Прошивка не найдена",
|
||||
"sv_SE": "Firmware hittades inte",
|
||||
"th_TH": "ไม่พบ เฟิร์มแวร์",
|
||||
"tr_TR": "Firmware bulunamadı",
|
||||
"uk_UA": "Прошивка не знайдена",
|
||||
"zh_CN": "未安装系统固件",
|
||||
"zh_TW": "找不到韌體"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "NoFirmwareFoundDescription",
|
||||
"Translations": {
|
||||
"ar_SA": "لم يتمكن ريوجينكس من العثور على أية برامج ثابتة مثبتة.",
|
||||
"de_DE": "Ryujinx konnte keine installierte Firmware finden!",
|
||||
"el_GR": "Το Ryujinx δεν κατάφερε να εντοπίσει κανένα εγκατεστημένο firmware.",
|
||||
"en_US": "Ryujinx was unable to find any firmwares installed.",
|
||||
"es_ES": "Ryujinx no pudo encontrar un firmware instalado.",
|
||||
"fr_FR": "Ryujinx n'a pas trouvé de firmware installé.",
|
||||
"he_IL": "ריוג'ינקס לא הצליחה למצוא קושחה מותקנת.",
|
||||
"it_IT": "Ryujinx non è riuscito a trovare alcun firmware installato.",
|
||||
"ja_JP": "インストールされたファームウェアが見つかりませんでした。",
|
||||
"ko_KR": "Ryujinx가 설치된 펌웨어를 찾을 수 없음.",
|
||||
"no_NO": "Ryujinx kunne ikke finne noen fastvare installert.",
|
||||
"pl_PL": "Ryujinx nie mógł znaleźć żadnego zainstalowanego firmware'u.",
|
||||
"pt_BR": "Ryujinx não conseguiu encontrar nenhum Firmware instalado.",
|
||||
"ru_RU": "Ryujinx не удалось найти ни одной установленной прошивки.",
|
||||
"sv_SE": "Ryujinx kunde inte hitta några installerade firmwares.",
|
||||
"th_TH": "Ryujinx ไม่พบ เฟิร์มแวร์ที่ติดตั้งไว้ในเครื่องของคุณ",
|
||||
"tr_TR": "Ryujinx yüklü herhangi firmware bulamadı.",
|
||||
"uk_UA": "Ryujinx не вдалося знайти жодної встановленої прошивки.",
|
||||
"zh_CN": "Ryujinx 模拟器未安装 Switch 系统固件。",
|
||||
"zh_TW": "Ryujinx 無法找到已安裝的任何韌體。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "FirmwareParsingFailed",
|
||||
"Translations": {
|
||||
"ar_SA": "خطأ في تحليل البرنامج الثابت",
|
||||
"de_DE": "Firmware-Analysierung-Fehler",
|
||||
"el_GR": "Σφάλμα ανάλυσης firmware",
|
||||
"en_US": "Firmware parsing error",
|
||||
"es_ES": "Error al analizar el Firmware",
|
||||
"fr_FR": "Erreur d'analyse du firmware",
|
||||
"he_IL": "שגיאת ניתוח קושחה",
|
||||
"it_IT": "Errore di analisi del firmware",
|
||||
"ja_JP": "ファームウェアのパーズエラー",
|
||||
"ko_KR": "펌웨어 구문 분석 오류",
|
||||
"no_NO": "Fastvare analysefeil",
|
||||
"pl_PL": "Błąd parsowania firmware'u",
|
||||
"pt_BR": "Erro de análise de firmware",
|
||||
"ru_RU": "Ошибка извлечения прошивки",
|
||||
"sv_SE": "Tolkningsfel i firmware",
|
||||
"th_TH": "เกิดข้อผิดพลาดในการวิเคราะห์เฟิร์มแวร์",
|
||||
"tr_TR": "Firmware çözümleme hatası",
|
||||
"uk_UA": "Помилка аналізу прошивки",
|
||||
"zh_CN": "固件文件解析出错",
|
||||
"zh_TW": "韌體解析錯誤"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "FirmwareParsingFailedDescription",
|
||||
"Translations": {
|
||||
"ar_SA": "لم يتمكن ريوجينكس من تحليل البرامج الثابتة المتوفرة. يحدث هذا عادة بسبب المفاتيح القديمة.",
|
||||
"de_DE": "Ryujinx konnte die zu verfügung gestellte Firmware nicht analysieren. Ein möglicher Grund dafür sind veraltete keys.",
|
||||
"el_GR": "Το Ryujinx δεν κατάφερε να αναλύσει το συγκεκριμένο firmware. Αυτό συνήθως οφείλετε σε ξεπερασμένα/παλιά κλειδιά.",
|
||||
"en_US": "Ryujinx was unable to parse the provided firmware. This is usually caused by outdated keys.",
|
||||
"es_ES": "Ryujinx no pudo analizar el firmware. Normalmente esto ocurre debido a keys desfasadas.",
|
||||
"fr_FR": "Ryujinx n'a pas pu analyser le firmware fourni. Cela est généralement dû à des clés obsolètes.",
|
||||
"he_IL": "ריוג'ינקס לא הצליחה לנתח את הקושחה שסופקה. זה נגרם בדרך כלל על ידי מפתחות לא עדכניים.",
|
||||
"it_IT": "Ryujinx non è riuscito ad analizzare il firmware. Questo di solito è causato da chiavi non aggiornate.",
|
||||
"ja_JP": "ファームウェアをパーズできませんでした.通常,古いキーが原因です.",
|
||||
"ko_KR": "Ryujinx가 제공된 펌웨어를 구문 분석하지 못했습니다. 일반적으로 오래된 키로 인해 발생합니다.",
|
||||
"no_NO": "Ryujinx klarte ikke å analysere levert fastvare. Dette er vanligvis forårsaket av utdaterte nøkler.",
|
||||
"pl_PL": "Ryujinx nie był w stanie zparsować dostarczonego firmware'u. Jest to zwykle spowodowane nieaktualnymi kluczami.",
|
||||
"pt_BR": "Ryujinx não conseguiu ler o Firmware fornecido. Geralmente isso é causado por chaves desatualizadas.",
|
||||
"ru_RU": "Ryujinx не удалось распаковать выбранную прошивку. Обычно это вызвано устаревшими ключами.",
|
||||
"sv_SE": "Ryujinx kunde inte tolka angiven firmware. Detta sker oftast med utdaterade nycklar.",
|
||||
"th_TH": "Ryujinx ไม่สามารถวิเคราะห์เฟิร์มแวร์ที่ให้มาได้ ซึ่งมักมีสาเหตุมาจากคีย์ที่เก่าจนเกินไป",
|
||||
"tr_TR": "Ryujinx temin edilen firmware'i çözümleyemedi. Bu durum genellikle güncel olmayan keys'den kaynaklanır.",
|
||||
"uk_UA": "Ryujinx не вдалося проаналізувати прошивку. Зазвичай це спричинено застарілими ключами.",
|
||||
"zh_CN": "Ryujinx 模拟器无法解密当前固件,一般是由于使用了旧版的密钥导致的。",
|
||||
"zh_TW": "Ryujinx 無法解析所提供的韌體。這通常是由於金鑰過時造成的。"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
1904
assets/Locales/KeyboardLayout.json
Normal file
1904
assets/Locales/KeyboardLayout.json
Normal file
File diff suppressed because it is too large
Load Diff
604
assets/Locales/MenuBar_Actions.json
Normal file
604
assets/Locales/MenuBar_Actions.json
Normal file
@@ -0,0 +1,604 @@
|
||||
{
|
||||
"Locales": [
|
||||
{
|
||||
"ID": "ActionsLabel",
|
||||
"Translations": {
|
||||
"ar_SA": "_الإجراءات",
|
||||
"de_DE": "_Aktionen",
|
||||
"el_GR": "_Δράσεις",
|
||||
"en_US": "_Actions",
|
||||
"es_ES": "_Acciones",
|
||||
"fr_FR": null,
|
||||
"he_IL": "_פעולות",
|
||||
"it_IT": "_Azioni",
|
||||
"ja_JP": "アクション(_A)",
|
||||
"ko_KR": "동작(_A)",
|
||||
"no_NO": "_Handlinger",
|
||||
"pl_PL": "_Akcje",
|
||||
"pt_BR": "_Ações",
|
||||
"ru_RU": "_Действия",
|
||||
"sv_SE": "_Åtgärder",
|
||||
"th_TH": "_การดำเนินการ",
|
||||
"tr_TR": "_Eylemler",
|
||||
"uk_UA": "_Дії",
|
||||
"zh_CN": "操作(_A)",
|
||||
"zh_TW": "動作(_A)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallKeysLabel",
|
||||
"Translations": {
|
||||
"ar_SA": "تثبيت المفاتيح",
|
||||
"de_DE": "Schlüssel installieren",
|
||||
"el_GR": "Εγκατάσταση Κλειδιών",
|
||||
"en_US": "Install Keys",
|
||||
"es_ES": "Instalar Claves",
|
||||
"fr_FR": "Installer des Clés",
|
||||
"he_IL": "התקנת מפתחות",
|
||||
"it_IT": "Installa chiavi",
|
||||
"ja_JP": "キーをインストール",
|
||||
"ko_KR": "설치 키",
|
||||
"no_NO": "Installere nøkler",
|
||||
"pl_PL": "Zainstaluj klucze",
|
||||
"pt_BR": "Instalar Chaves",
|
||||
"ru_RU": "Установить ключи",
|
||||
"sv_SE": "Installera nycklar",
|
||||
"th_TH": "ติดตั้ง Keys",
|
||||
"tr_TR": "Anahtarları Yükle",
|
||||
"uk_UA": "Встановити Ключі",
|
||||
"zh_CN": "安装密匙",
|
||||
"zh_TW": "安裝金鑰"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallKeysFromFileButton",
|
||||
"Translations": {
|
||||
"ar_SA": "KEYS...",
|
||||
"de_DE": null,
|
||||
"el_GR": null,
|
||||
"en_US": "KEYS...",
|
||||
"es_ES": null,
|
||||
"fr_FR": null,
|
||||
"he_IL": "KEYS...",
|
||||
"it_IT": null,
|
||||
"ja_JP": null,
|
||||
"ko_KR": null,
|
||||
"no_NO": null,
|
||||
"pl_PL": null,
|
||||
"pt_BR": null,
|
||||
"ru_RU": null,
|
||||
"sv_SE": null,
|
||||
"th_TH": null,
|
||||
"tr_TR": null,
|
||||
"uk_UA": null,
|
||||
"zh_CN": null,
|
||||
"zh_TW": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallKeysFromFolderButton",
|
||||
"Translations": {
|
||||
"ar_SA": "مجلد...",
|
||||
"de_DE": "Verzeichnis...",
|
||||
"el_GR": "Φάκελος...",
|
||||
"en_US": "Folder...",
|
||||
"es_ES": "Carpeta...",
|
||||
"fr_FR": "Dossier...",
|
||||
"he_IL": "תיקייה...",
|
||||
"it_IT": "Cartella...",
|
||||
"ja_JP": "フォルダー...",
|
||||
"ko_KR": "폴더...",
|
||||
"no_NO": "Mappe...",
|
||||
"pl_PL": "Katalog...",
|
||||
"pt_BR": "Diretório...",
|
||||
"ru_RU": "Папка...",
|
||||
"sv_SE": "Katalog...",
|
||||
"th_TH": "โฟลเดอร์...",
|
||||
"tr_TR": "Klasör...",
|
||||
"uk_UA": "Тека...",
|
||||
"zh_CN": "文件夹...",
|
||||
"zh_TW": "資料夾..."
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallFirmwareLabel",
|
||||
"Translations": {
|
||||
"ar_SA": "تثبيت البرنامج الثابت",
|
||||
"de_DE": "Firmware installieren",
|
||||
"el_GR": "Εγκατάσταση Firmware",
|
||||
"en_US": "Install Firmware",
|
||||
"es_ES": "Instalar Firmware",
|
||||
"fr_FR": "Installer le Firmware",
|
||||
"he_IL": "התקן קושחה",
|
||||
"it_IT": "Installa firmware",
|
||||
"ja_JP": "ファームウェアをインストール",
|
||||
"ko_KR": "펌웨어 설치",
|
||||
"no_NO": "Installer fastvare",
|
||||
"pl_PL": "Zainstaluj oprogramowanie",
|
||||
"pt_BR": "Instalar Firmware",
|
||||
"ru_RU": "Установить прошивку",
|
||||
"sv_SE": "Installera firmware",
|
||||
"th_TH": "ติดตั้งเฟิร์มแวร์",
|
||||
"tr_TR": "Yazılım Yükle",
|
||||
"uk_UA": "Встановити прошивку",
|
||||
"zh_CN": "安装系统固件",
|
||||
"zh_TW": "安裝韌體"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallFirmwareFromFileButton",
|
||||
"Translations": {
|
||||
"ar_SA": "XCI أو ZIP...",
|
||||
"de_DE": "XCI oder ZIP...",
|
||||
"el_GR": "XCI ή ZIP...",
|
||||
"en_US": "XCI or ZIP...",
|
||||
"es_ES": "XCI o ZIP...",
|
||||
"fr_FR": "XCI ou ZIP...",
|
||||
"he_IL": "XCI או ZIP...",
|
||||
"it_IT": "XCI o ZIP...",
|
||||
"ja_JP": "XCI または ZIP...",
|
||||
"ko_KR": "XCI 또는 ZIP...",
|
||||
"no_NO": "XCI eller ZIP...",
|
||||
"pl_PL": "XCI lub ZIP...",
|
||||
"pt_BR": "XCI ou ZIP...",
|
||||
"ru_RU": "XCI или ZIP...",
|
||||
"sv_SE": "XCI eller ZIP...",
|
||||
"th_TH": "XCI หรือ ZIP...",
|
||||
"tr_TR": "XCI veya ZIP...",
|
||||
"uk_UA": "XCI або ZIP...",
|
||||
"zh_CN": "XCI 或 ZIP...",
|
||||
"zh_TW": "XCI 或 ZIP..."
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallFirmwareFromFolderButton",
|
||||
"Translations": {
|
||||
"ar_SA": "مجلد...",
|
||||
"de_DE": "Verzeichnis...",
|
||||
"el_GR": "Φάκελος...",
|
||||
"en_US": "Folder...",
|
||||
"es_ES": "Carpeta...",
|
||||
"fr_FR": "Dossier...",
|
||||
"he_IL": "תיקייה...",
|
||||
"it_IT": "Cartella...",
|
||||
"ja_JP": "フォルダー...",
|
||||
"ko_KR": "폴더...",
|
||||
"no_NO": "Mappe...",
|
||||
"pl_PL": "Katalog...",
|
||||
"pt_BR": "Diretório...",
|
||||
"ru_RU": "Папка...",
|
||||
"sv_SE": "Katalog...",
|
||||
"th_TH": "โฟลเดอร์...",
|
||||
"tr_TR": "Klasör...",
|
||||
"uk_UA": "Тека...",
|
||||
"zh_CN": "文件夹...",
|
||||
"zh_TW": "資料夾..."
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ToolsLabel",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "Werkzeuge",
|
||||
"el_GR": "",
|
||||
"en_US": "Tools",
|
||||
"es_ES": "Herramientas",
|
||||
"fr_FR": "Outils",
|
||||
"he_IL": "",
|
||||
"it_IT": "Strumenti",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "도구",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "Ferramentas",
|
||||
"ru_RU": "Инструменты",
|
||||
"sv_SE": "Verktyg",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "工具",
|
||||
"zh_TW": "工具"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "MiiEditorButton",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "Mii-Editor",
|
||||
"el_GR": "",
|
||||
"en_US": "Mii Editor",
|
||||
"es_ES": "Editor de Mii",
|
||||
"fr_FR": "Éditeur de Mii",
|
||||
"he_IL": "",
|
||||
"it_IT": "Editor di Mii",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "Mii 편집기",
|
||||
"no_NO": "Mii-redigerer",
|
||||
"pl_PL": "Edytor Mii",
|
||||
"pt_BR": "Editor de Mii",
|
||||
"ru_RU": "Редактор Mii",
|
||||
"sv_SE": "Mii-redigerare",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "Редактор Mii",
|
||||
"zh_CN": "Mii 编辑器",
|
||||
"zh_TW": "Mii 編輯器"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "XCITrimmerButton",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "XCI-Dateien trimmen",
|
||||
"el_GR": "",
|
||||
"en_US": "Trim XCI Files",
|
||||
"es_ES": "Recortar Archivos XCI",
|
||||
"fr_FR": "Réduire les Fichiers XCI",
|
||||
"he_IL": "",
|
||||
"it_IT": "Riduci dimensioni dei file XCI",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "XCI 파일 트리머",
|
||||
"no_NO": "Trim XCI-filer",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "Reduzir Arquivos XCI",
|
||||
"ru_RU": "Обрезать XCI файлы",
|
||||
"sv_SE": "Optimera XCI-filer",
|
||||
"th_TH": "ตัดแต่งไฟล์ XCI",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "Обрізати XCI файли",
|
||||
"zh_CN": "瘦身 XCI 文件",
|
||||
"zh_TW": "修剪 XCI 檔案"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "PauseEmulationButton",
|
||||
"Translations": {
|
||||
"ar_SA": "إيقاف التشغيل مؤقتًا",
|
||||
"de_DE": "Emulation pausieren",
|
||||
"el_GR": "Παύση προσομοίωσης",
|
||||
"en_US": "Pause Emulation",
|
||||
"es_ES": "Pausar Emulación",
|
||||
"fr_FR": "Pauser l'Émulation",
|
||||
"he_IL": "השהיית האמולציה",
|
||||
"it_IT": "Pausa emulazione",
|
||||
"ja_JP": "エミュレーション一時停止",
|
||||
"ko_KR": "에뮬레이션 일시중지",
|
||||
"no_NO": "Pause Emulatoren",
|
||||
"pl_PL": "Wstrzymaj emulację",
|
||||
"pt_BR": "Pausar emulação",
|
||||
"ru_RU": "Пауза эмуляции",
|
||||
"sv_SE": "Pausa emuleringen",
|
||||
"th_TH": "พักการจำลอง",
|
||||
"tr_TR": "Emülasyonu Duraklat",
|
||||
"uk_UA": "Пауза емуляції",
|
||||
"zh_CN": "暂停模拟",
|
||||
"zh_TW": "暫停模擬"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ResumeEmulationButton",
|
||||
"Translations": {
|
||||
"ar_SA": "استئناف المحاكاة",
|
||||
"de_DE": "Emulation fortsetzen",
|
||||
"el_GR": "Συνέχιση προσομοίωσης",
|
||||
"en_US": "Resume Emulation",
|
||||
"es_ES": "Reanudar Emulación",
|
||||
"fr_FR": "Reprendre l'Émulation",
|
||||
"he_IL": "המשך האמולציה",
|
||||
"it_IT": "Riprendi l'emulazione",
|
||||
"ja_JP": "エミュレーション再開",
|
||||
"ko_KR": "에뮬레이션 다시 시작",
|
||||
"no_NO": "Gjenoppta emuleringen",
|
||||
"pl_PL": "Wznów emulację",
|
||||
"pt_BR": "Retomar emulação",
|
||||
"ru_RU": "Продолжить эмуляцию",
|
||||
"sv_SE": "Återuppta emuleringen",
|
||||
"th_TH": "ดำเนินการจำลองต่อ",
|
||||
"tr_TR": "Emülasyonu Sürdür",
|
||||
"uk_UA": "Продовжити емуляцію",
|
||||
"zh_CN": "继续模拟",
|
||||
"zh_TW": "繼續模擬"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "StopEmulationButton",
|
||||
"Translations": {
|
||||
"ar_SA": "إيقاف المحاكاة",
|
||||
"de_DE": "Emulation beenden",
|
||||
"el_GR": "Διακοπή Εξομοίωσης",
|
||||
"en_US": "Stop Emulation",
|
||||
"es_ES": "Detener Emulación",
|
||||
"fr_FR": "Arrêter l'Émulation",
|
||||
"he_IL": "עצור אמולציה",
|
||||
"it_IT": "Arresta l'emulazione",
|
||||
"ja_JP": "エミュレーションを中止",
|
||||
"ko_KR": "에뮬레이션 중지",
|
||||
"no_NO": "Stopp Emulering",
|
||||
"pl_PL": "Zatrzymaj emulację",
|
||||
"pt_BR": "Parar a Emulação",
|
||||
"ru_RU": "Остановить эмуляцию",
|
||||
"sv_SE": "Stoppa emulering",
|
||||
"th_TH": "หยุดการจำลอง",
|
||||
"tr_TR": "Emülasyonu Durdur",
|
||||
"uk_UA": "Зупинити емуляцію",
|
||||
"zh_CN": "停止模拟",
|
||||
"zh_TW": "停止模擬"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "RestartEmulationButton",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "Emulation neustarten",
|
||||
"el_GR": "",
|
||||
"en_US": "Restart Emulation",
|
||||
"es_ES": "Reiniciar Emulación",
|
||||
"fr_FR": "Redémarrer l'Émulation",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "Перезапустить эмуляцию",
|
||||
"sv_SE": "Starta om emulering",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "重启模拟",
|
||||
"zh_TW": "重新啟動模擬"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ScanAmiiboButton",
|
||||
"Translations": {
|
||||
"ar_SA": "مسح Amiibo",
|
||||
"de_DE": "Amiibo scannen",
|
||||
"el_GR": "Σάρωση Amiibo",
|
||||
"en_US": "Scan Amiibo",
|
||||
"es_ES": "Escanear Amiibo",
|
||||
"fr_FR": "Scanner un Amiibo",
|
||||
"he_IL": "סרוק אמיבו",
|
||||
"it_IT": "Scansiona un Amiibo",
|
||||
"ja_JP": "Amiibo をスキャン",
|
||||
"ko_KR": "Amiibo 스캔",
|
||||
"no_NO": "Skann en Amiibo",
|
||||
"pl_PL": "Skanuj Amiibo",
|
||||
"pt_BR": "Escanear um Amiibo",
|
||||
"ru_RU": "Сканировать Amiibo",
|
||||
"sv_SE": "Skanna en Amiibo",
|
||||
"th_TH": "สแกนหา Amiibo",
|
||||
"tr_TR": "Bir Amiibo Tara",
|
||||
"uk_UA": "Сканувати Amiibo",
|
||||
"zh_CN": "扫描 Amiibo",
|
||||
"zh_TW": "掃描 Amiibo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ScanAmiiboFromBinButton",
|
||||
"Translations": {
|
||||
"ar_SA": "مسح Amiibo (.BIN)",
|
||||
"de_DE": "Amiibo scannen (.BIN)",
|
||||
"el_GR": "Σάρωση Amiibo (.BIN)",
|
||||
"en_US": "Scan Amiibo (.BIN)",
|
||||
"es_ES": "Escanear un Amiibo (.BIN)",
|
||||
"fr_FR": "Scanner un Amiibo (.BIN)",
|
||||
"he_IL": "סרוק Amiibo (.BIN)",
|
||||
"it_IT": "Scansiona un Amiibo (.BIN)",
|
||||
"ja_JP": "Amiibo をスキャン (.BIN)",
|
||||
"ko_KR": "Amiibo 스캔 (.BIN)",
|
||||
"no_NO": "Skann en Amiibo (.BIN)",
|
||||
"pl_PL": "Skanuj Amiibo (.BIN)",
|
||||
"pt_BR": "Escaneie um Amiibo (.BIN)",
|
||||
"ru_RU": "Сканировать Amiibo (.BIN)",
|
||||
"sv_SE": "Skanna en Amiibo (.BIN)",
|
||||
"th_TH": "สแกนอามีโบ (.BIN)",
|
||||
"tr_TR": "Amiibo Tara (.BIN)",
|
||||
"uk_UA": "Сканувати Amiibo (.BIN)",
|
||||
"zh_CN": "扫描 Amiibo (.BIN)",
|
||||
"zh_TW": "掃瞄 Amiibo (.BIN)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ScanSkylanderButton",
|
||||
"Translations": {
|
||||
"ar_SA": "فحص Skylander",
|
||||
"de_DE": "Skylander scannen",
|
||||
"el_GR": "Σάρωση Skylander",
|
||||
"en_US": "Scan Skylander",
|
||||
"es_ES": "Escanear Skylander",
|
||||
"fr_FR": "Scanner un Skylander",
|
||||
"he_IL": "סרוק אמיבו",
|
||||
"it_IT": "Scansiona un Skylander",
|
||||
"ja_JP": "Skylander をスキャン",
|
||||
"ko_KR": "Skylander 스캔",
|
||||
"no_NO": "Skann en Skylander",
|
||||
"pl_PL": "Skanuj Skylander",
|
||||
"pt_BR": "Escanear um Skylander",
|
||||
"ru_RU": "Сканировать Skylander",
|
||||
"sv_SE": "Skanna en Skylander",
|
||||
"th_TH": "สแกนหา Skylander",
|
||||
"tr_TR": "Bir Skylander Tara",
|
||||
"uk_UA": "Сканувати Skylander",
|
||||
"zh_CN": "扫描 Skylander",
|
||||
"zh_TW": "掃描 Skylander"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "RemoveSkylanderButton",
|
||||
"Translations": {
|
||||
"ar_SA": "إزالة Skylander",
|
||||
"de_DE": "Skylander entfernen",
|
||||
"el_GR": "Αφαίρεση Skylander",
|
||||
"en_US": "Remove Skylander",
|
||||
"es_ES": "Eliminar Skylander",
|
||||
"fr_FR": "Supprimer un Skylander",
|
||||
"he_IL": "הסר Skylander",
|
||||
"it_IT": "Rimuovi Skylander",
|
||||
"ja_JP": "Skylander を削除",
|
||||
"ko_KR": "Skylander 제거",
|
||||
"no_NO": "Fjern Skylander",
|
||||
"pl_PL": "Usuń Skylander",
|
||||
"pt_BR": "Remover um Skylander",
|
||||
"ru_RU": "Удалить Skylander",
|
||||
"sv_SE": "Ta bort Skylander",
|
||||
"th_TH": "ลบ Skylander",
|
||||
"tr_TR": "Skylander'ı Kaldır",
|
||||
"uk_UA": "Видалити Skylander",
|
||||
"zh_CN": "移除 Skylander",
|
||||
"zh_TW": "移除 Skylander"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "TakeScreenshotButton",
|
||||
"Translations": {
|
||||
"ar_SA": "أخذ لقطة للشاشة",
|
||||
"de_DE": "Screenshot aufnehmen",
|
||||
"el_GR": "Λήψη Στιγμιότυπου",
|
||||
"en_US": "Take Screenshot",
|
||||
"es_ES": "Captura de Pantalla",
|
||||
"fr_FR": "Prendre une Capture d'Écran",
|
||||
"he_IL": "צלם מסך",
|
||||
"it_IT": "Cattura uno screenshot",
|
||||
"ja_JP": "スクリーンショットを撮影",
|
||||
"ko_KR": "스크린샷 찍기",
|
||||
"no_NO": "Ta skjermbilde",
|
||||
"pl_PL": "Zrób zrzut ekranu",
|
||||
"pt_BR": "Tirar Captura de tela",
|
||||
"ru_RU": "Сделать снимок экрана",
|
||||
"sv_SE": "Ta skärmbild",
|
||||
"th_TH": "ถ่ายภาพหน้าจอ",
|
||||
"tr_TR": "Ekran Görüntüsü Al",
|
||||
"uk_UA": "Зробити знімок екрана",
|
||||
"zh_CN": "保存截屏",
|
||||
"zh_TW": "儲存擷取畫面"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "HideUiButton",
|
||||
"Translations": {
|
||||
"ar_SA": "إخفاء واجهة المستخدم",
|
||||
"de_DE": "Oberfläche ausblenden",
|
||||
"el_GR": "Απόκρυψη UI",
|
||||
"en_US": "Hide UI",
|
||||
"es_ES": "Ocultar Interfaz",
|
||||
"fr_FR": "Masquer l'Interface",
|
||||
"he_IL": "הסתר ממשק משתמש ",
|
||||
"it_IT": "Nascondi l'interfaccia",
|
||||
"ja_JP": "UIを隠す",
|
||||
"ko_KR": "UI 숨기기",
|
||||
"no_NO": "Skjul brukergrensesnitt",
|
||||
"pl_PL": "Ukryj interfejs użytkownika",
|
||||
"pt_BR": "Esconder Interface",
|
||||
"ru_RU": "Скрыть интерфейс",
|
||||
"sv_SE": "Dölj gränssnittet",
|
||||
"th_TH": "ซ่อน UI",
|
||||
"tr_TR": "Arayüzü Gizle",
|
||||
"uk_UA": "Сховати інтерфейс",
|
||||
"zh_CN": "隐藏菜单栏和状态栏",
|
||||
"zh_TW": "隱藏 UI"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "StartRenderDocCaptureButton",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "RenderDoc Frame-Aufnahme starten",
|
||||
"el_GR": "",
|
||||
"en_US": "Start RenderDoc Frame Capture",
|
||||
"es_ES": "Iniciar una captura de fotograma de RenderDoc",
|
||||
"fr_FR": "Démarrer une capture de trame RenderDoc",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "RenderDoc 프레임 캡처 시작",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "Запустить захват кадра RenderDoc",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "启动 RenderDoc 帧捕获",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "EndRenderDocCaptureButton",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "RenderDoc Frame-Aufnahme beenden",
|
||||
"el_GR": "",
|
||||
"en_US": "End RenderDoc Frame Capture",
|
||||
"es_ES": "Detener la captura de fotograma de RenderDoc",
|
||||
"fr_FR": "Arrêter la capture de trame RenderDoc",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "RenderDoc 프레임 캡처 종료",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "Завершить захват кадра RenderDoc",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "结束 RenderDoc 帧捕获",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DiscardRenderDocCaptureButton",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "RenderDoc Frame-Aufnahme verwerfen",
|
||||
"el_GR": "",
|
||||
"en_US": "Discard RenderDoc Frame Capture",
|
||||
"es_ES": "Descartar la captura de fotograma de RenderDoc",
|
||||
"fr_FR": "Supprimer la capture de trame RenderDoc",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "RenderDoc 프레임 캡처 폐기",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "Отменить захват кадра RenderDoc",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "丢弃 RenderDoc 帧捕获",
|
||||
"zh_TW": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DiscardRenderDocCaptureToolTip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "Beendet die jetzige RenderDoc Frame-Aufnahme, verwirft sofort das Ergebnis.",
|
||||
"el_GR": "",
|
||||
"en_US": "Ends the currently active RenderDoc Frame Capture, immediately discarding its result.",
|
||||
"es_ES": "Finaliza la captura de fotograma de RenderDoc actualmente activa y descarta inmediatamente su resultado.",
|
||||
"fr_FR": "Met fin à la capture de trame RenderDoc en cours, en supprimant immédiatement son résultat.",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "현재 활성화된 RenderDoc 프레임 캡처를 종료하고 결과를 즉시 폐기합니다.",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "Завершает текущий активный захват кадра RenderDoc и немедленно удаляет его результат.",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "结束当前正在进行的 RenderDoc 帧捕获,并立即丢弃其结果。",
|
||||
"zh_TW": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
79
assets/Locales/MenuBar_File.json
Normal file
79
assets/Locales/MenuBar_File.json
Normal file
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"Locales": [
|
||||
{
|
||||
"ID": "ManageFileTypes",
|
||||
"Translations": {
|
||||
"ar_SA": "إدارة أنواع الملفات",
|
||||
"de_DE": "Dateitypen verwalten",
|
||||
"el_GR": "Διαχείριση τύπων αρχείων",
|
||||
"en_US": "Manage File Types",
|
||||
"es_ES": "Administrar Tipos de Archivo",
|
||||
"fr_FR": "Gérer les Types de Fichiers",
|
||||
"he_IL": "ניהול סוגי קבצים",
|
||||
"it_IT": "Gestisci i tipi di file",
|
||||
"ja_JP": "ファイル形式を管理",
|
||||
"ko_KR": "파일 형식 관리",
|
||||
"no_NO": "Behandle filtyper",
|
||||
"pl_PL": "Zarządzaj rodzajami plików",
|
||||
"pt_BR": "Gerenciar Tipos de Arquivos",
|
||||
"ru_RU": "Управление типами файлов",
|
||||
"sv_SE": "Hantera filtyper",
|
||||
"th_TH": "จัดการประเภทไฟล์",
|
||||
"tr_TR": "Dosya uzantılarını yönet",
|
||||
"uk_UA": "Керувати типами файлів",
|
||||
"zh_CN": "管理文件扩展名",
|
||||
"zh_TW": "管理檔案類型"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "InstallFileTypes",
|
||||
"Translations": {
|
||||
"ar_SA": "تثبيت أنواع الملفات",
|
||||
"de_DE": "Dateitypen installieren",
|
||||
"el_GR": "Εγκαταστήσετε τύπους αρχείων.",
|
||||
"en_US": "Install File Types",
|
||||
"es_ES": "Instalar Tipos de Archivo",
|
||||
"fr_FR": "Installer des Types de Fichiers",
|
||||
"he_IL": "סוגי קבצי התקנה",
|
||||
"it_IT": "Installa i tipi di file",
|
||||
"ja_JP": "ファイル形式をインストール",
|
||||
"ko_KR": "파일 형식 설치",
|
||||
"no_NO": "Installer filtyper",
|
||||
"pl_PL": "Typy plików instalacyjnych",
|
||||
"pt_BR": "Instalar tipos de arquivos",
|
||||
"ru_RU": "Установить типы файлов",
|
||||
"sv_SE": "Installera filtyper",
|
||||
"th_TH": "ติดตั้งประเภทไฟล์",
|
||||
"tr_TR": "Dosya uzantılarını yükle",
|
||||
"uk_UA": "Встановити типи файлів",
|
||||
"zh_CN": "关联文件扩展名",
|
||||
"zh_TW": "安裝檔案類型"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "UninstallFileTypes",
|
||||
"Translations": {
|
||||
"ar_SA": "إزالة أنواع الملفات",
|
||||
"de_DE": "Dateitypen deinstallieren",
|
||||
"el_GR": "Απεγκαταστήσετε τύπους αρχείων",
|
||||
"en_US": "Uninstall File Types",
|
||||
"es_ES": "Desinstalar Tipos de Archivo",
|
||||
"fr_FR": "Désinstaller des Types de Fichiers",
|
||||
"he_IL": "סוגי קבצי הסרה",
|
||||
"it_IT": "Disinstalla i tipi di file",
|
||||
"ja_JP": "ファイル形式をアンインストール",
|
||||
"ko_KR": "파일 형식 제거",
|
||||
"no_NO": "Avinstaller filtyper",
|
||||
"pl_PL": "Typy plików dezinstalacyjnych",
|
||||
"pt_BR": "Desinstalar tipos de arquivos",
|
||||
"ru_RU": "Удалить типы файлов",
|
||||
"sv_SE": "Avinstallera filtyper",
|
||||
"th_TH": "ถอนการติดตั้งประเภทไฟล์",
|
||||
"tr_TR": "Dosya uzantılarını kaldır",
|
||||
"uk_UA": "Видалити типи файлів",
|
||||
"zh_CN": "取消关联扩展名",
|
||||
"zh_TW": "移除檔案類型"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
{
|
||||
"Locales": [
|
||||
{
|
||||
"ID": "MenuBarActions_StartCapture",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "RenderDoc Frame-Aufnahme starten",
|
||||
"el_GR": "",
|
||||
"en_US": "Start RenderDoc Frame Capture",
|
||||
"es_ES": "Iniciar una captura de fotograma de RenderDoc",
|
||||
"fr_FR": "Démarrer une capture de trame RenderDoc",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "RenderDoc 프레임 캡처 시작",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "启动 RenderDoc 帧捕获",
|
||||
"zh_TW": "啟動 RenderDoc 畫格擷取"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "MenuBarActions_EndCapture",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "RenderDoc Frame-Aufnahme beenden",
|
||||
"el_GR": "",
|
||||
"en_US": "End RenderDoc Frame Capture",
|
||||
"es_ES": "Detener la captura de fotograma de RenderDoc",
|
||||
"fr_FR": "Arrêter la capture de trame RenderDoc",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "RenderDoc 프레임 캡처 종료",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "结束 RenderDoc 帧捕获",
|
||||
"zh_TW": "停止 RenderDoc 畫格擷取"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "MenuBarActions_DiscardCapture",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "RenderDoc Frame-Aufnahme verwerfen",
|
||||
"el_GR": "",
|
||||
"en_US": "Discard RenderDoc Frame Capture",
|
||||
"es_ES": "Descartar la captura de fotograma de RenderDoc",
|
||||
"fr_FR": "Supprimer la capture de trame RenderDoc",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "RenderDoc 프레임 캡처 폐기",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "丢弃 RenderDoc 帧捕获",
|
||||
"zh_TW": "捨棄 RenderDoc 畫格擷取"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "MenuBarActions_DiscardCapture_ToolTip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "Beendet die jetzige RenderDoc Frame-Aufnahme, verwirft sofort das Ergebnis.",
|
||||
"el_GR": "",
|
||||
"en_US": "Ends the currently active RenderDoc Frame Capture, immediately discarding its result.",
|
||||
"es_ES": "Finaliza la captura de fotograma de RenderDoc actualmente activa y descarta inmediatamente su resultado.",
|
||||
"fr_FR": "Met fin à la capture de trame RenderDoc en cours, en supprimant immédiatement son résultat.",
|
||||
"he_IL": "",
|
||||
"it_IT": "",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "현재 활성화된 RenderDoc 프레임 캡처를 종료하고 결과를 즉시 폐기합니다.",
|
||||
"no_NO": "",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "",
|
||||
"ru_RU": "",
|
||||
"sv_SE": "",
|
||||
"th_TH": "",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "",
|
||||
"zh_CN": "结束当前正在进行的 RenderDoc 帧捕获,并立即丢弃其结果。",
|
||||
"zh_TW": "停止正在執行的 RenderDoc 畫格擷取,且立即捨棄其結果。"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
29
assets/Locales/StatusBar.json
Normal file
29
assets/Locales/StatusBar.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"Locales": [
|
||||
{
|
||||
"ID": "FirmwareVersion",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"el_GR": "",
|
||||
"en_US": "Firmware Version: {0}",
|
||||
"es_ES": "Versión del Firmware: {0}",
|
||||
"fr_FR": "Version du Firmware : {0}",
|
||||
"he_IL": "",
|
||||
"it_IT": "Versione firmware: {0}",
|
||||
"ja_JP": "",
|
||||
"ko_KR": "펌웨어 버전 : {0}",
|
||||
"no_NO": "Fastvareversjon: {0}",
|
||||
"pl_PL": "",
|
||||
"pt_BR": "Versão do Firmware: {0}",
|
||||
"ru_RU": "Версия прошивки: {0}",
|
||||
"sv_SE": "Firmware-version: {0}",
|
||||
"th_TH": "เวอร์ชันเฟิร์มแวร์: {0}",
|
||||
"tr_TR": "",
|
||||
"uk_UA": "Версія прошивки: {0}",
|
||||
"zh_CN": "系统固件版本:{0}",
|
||||
"zh_TW": "系統韌體版本: {0}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -47,14 +47,12 @@ def get_new_name(
|
||||
input_component = str(input_dylib_path).replace(str(input_directory), "")[1:]
|
||||
return Path(os.path.join(output_directory, input_component))
|
||||
|
||||
|
||||
def is_fat_file(dylib_path: Path) -> str:
|
||||
res = subprocess.check_output([LIPO, "-info", str(dylib_path.absolute())]).decode(
|
||||
"utf-8"
|
||||
)
|
||||
|
||||
return not res.split("\n")[0].startswith("Non-fat file")
|
||||
|
||||
def get_archs(dylib_path: Path) -> list[str]:
|
||||
res = subprocess.check_output([LIPO, "-info", str(dylib_path)]).decode("utf-8")
|
||||
if res.startswith("Non-fat file"):
|
||||
return [res.split(":")[-1].strip()]
|
||||
else:
|
||||
return res.split("are:")[-1].strip().split()
|
||||
|
||||
def construct_universal_dylib(
|
||||
arm64_input_dylib_path: Path, x86_64_input_dylib_path: Path, output_dylib_path: Path
|
||||
@@ -69,11 +67,12 @@ def construct_universal_dylib(
|
||||
os.path.basename(arm64_input_dylib_path.resolve()), output_dylib_path
|
||||
)
|
||||
else:
|
||||
if is_fat_file(arm64_input_dylib_path) or not x86_64_input_dylib_path.exists():
|
||||
with open(output_dylib_path, "wb") as dst:
|
||||
with open(arm64_input_dylib_path, "rb") as src:
|
||||
dst.write(src.read())
|
||||
else:
|
||||
arm64_archs = get_archs(arm64_input_dylib_path)
|
||||
x86_64_archs = get_archs(x86_64_input_dylib_path) if x86_64_input_dylib_path.exists() else []
|
||||
|
||||
if "arm64" in arm64_archs and "x86_64" in arm64_archs:
|
||||
shutil.copy2(arm64_input_dylib_path, output_dylib_path)
|
||||
elif x86_64_archs:
|
||||
subprocess.check_call(
|
||||
[
|
||||
LIPO,
|
||||
|
||||
@@ -110,7 +110,7 @@ namespace ARMeilleure.Translation.PTC
|
||||
Profiler.Wait();
|
||||
Profiler.ClearEntries();
|
||||
|
||||
Logger.Info?.Print(LogClass.Ptc, $"Initializing Profiled Persistent Translation Cache (enabled: {enabled}).");
|
||||
Logger.Info?.Print(LogClass.Ptc, $"Initializing Profiled Persistent Translation Cache v{InternalVersion}\n\t\t (title: {titleIdText}, version: '{displayVersion}', selector: '{cacheSelector}', enabled: {enabled}).");
|
||||
|
||||
if (!enabled || string.IsNullOrEmpty(titleIdText) || titleIdText == TitleIdTextDefault)
|
||||
{
|
||||
@@ -129,8 +129,6 @@ namespace ARMeilleure.Translation.PTC
|
||||
DisplayVersion = !string.IsNullOrEmpty(displayVersion) ? displayVersion : DisplayVersionDefault;
|
||||
_memoryMode = memoryMode;
|
||||
|
||||
Logger.Info?.Print(LogClass.Ptc, $"PPTC (v{InternalVersion}) Profile: {DisplayVersion}-{cacheSelector}");
|
||||
|
||||
string workPathActual = Path.Combine(AppDataManager.GamesDirPath, TitleIdText, "cache", "cpu", ActualDir);
|
||||
string workPathBackup = Path.Combine(AppDataManager.GamesDirPath, TitleIdText, "cache", "cpu", BackupDir);
|
||||
|
||||
|
||||
@@ -16,5 +16,10 @@ namespace Ryujinx.Common.Configuration.Hid.Controller
|
||||
/// Enable Rumble
|
||||
/// </summary>
|
||||
public bool EnableRumble { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable HD Rumble support
|
||||
/// </summary
|
||||
public bool UseHDRumble { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace Ryujinx.Common.Configuration.Hid.Keyboard
|
||||
{
|
||||
public class StandardKeyboardInputConfig : GenericKeyboardInputConfig<Key> { }
|
||||
public class StandardKeyboardInputConfig : GenericKeyboardInputConfig<PhysicalKey> { }
|
||||
}
|
||||
|
||||
142
src/Ryujinx.Common/Configuration/Hid/PhysicalKey.cs
Normal file
142
src/Ryujinx.Common/Configuration/Hid/PhysicalKey.cs
Normal file
@@ -0,0 +1,142 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Common.Configuration.Hid
|
||||
{
|
||||
[JsonConverter(typeof(JsonStringEnumConverter<PhysicalKey>))]
|
||||
public enum PhysicalKey
|
||||
{
|
||||
Unknown,
|
||||
ShiftLeft,
|
||||
ShiftRight,
|
||||
ControlLeft,
|
||||
ControlRight,
|
||||
AltLeft,
|
||||
AltRight,
|
||||
WinLeft,
|
||||
WinRight,
|
||||
Menu,
|
||||
F1,
|
||||
F2,
|
||||
F3,
|
||||
F4,
|
||||
F5,
|
||||
F6,
|
||||
F7,
|
||||
F8,
|
||||
F9,
|
||||
F10,
|
||||
F11,
|
||||
F12,
|
||||
F13,
|
||||
F14,
|
||||
F15,
|
||||
F16,
|
||||
F17,
|
||||
F18,
|
||||
F19,
|
||||
F20,
|
||||
F21,
|
||||
F22,
|
||||
F23,
|
||||
F24,
|
||||
F25,
|
||||
F26,
|
||||
F27,
|
||||
F28,
|
||||
F29,
|
||||
F30,
|
||||
F31,
|
||||
F32,
|
||||
F33,
|
||||
F34,
|
||||
F35,
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
Enter,
|
||||
Escape,
|
||||
Space,
|
||||
Tab,
|
||||
BackSpace,
|
||||
Insert,
|
||||
Delete,
|
||||
PageUp,
|
||||
PageDown,
|
||||
Home,
|
||||
End,
|
||||
CapsLock,
|
||||
ScrollLock,
|
||||
PrintScreen,
|
||||
Pause,
|
||||
NumLock,
|
||||
Clear,
|
||||
Keypad0,
|
||||
Keypad1,
|
||||
Keypad2,
|
||||
Keypad3,
|
||||
Keypad4,
|
||||
Keypad5,
|
||||
Keypad6,
|
||||
Keypad7,
|
||||
Keypad8,
|
||||
Keypad9,
|
||||
KeypadDivide,
|
||||
KeypadMultiply,
|
||||
KeypadSubtract,
|
||||
KeypadAdd,
|
||||
KeypadDecimal,
|
||||
KeypadEnter,
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
D,
|
||||
E,
|
||||
F,
|
||||
G,
|
||||
H,
|
||||
I,
|
||||
J,
|
||||
K,
|
||||
L,
|
||||
M,
|
||||
N,
|
||||
O,
|
||||
P,
|
||||
Q,
|
||||
R,
|
||||
S,
|
||||
T,
|
||||
U,
|
||||
V,
|
||||
W,
|
||||
X,
|
||||
Y,
|
||||
Z,
|
||||
Number0,
|
||||
Number1,
|
||||
Number2,
|
||||
Number3,
|
||||
Number4,
|
||||
Number5,
|
||||
Number6,
|
||||
Number7,
|
||||
Number8,
|
||||
Number9,
|
||||
Tilde,
|
||||
Grave,
|
||||
Minus,
|
||||
Plus,
|
||||
BracketLeft,
|
||||
BracketRight,
|
||||
Semicolon,
|
||||
Quote,
|
||||
Comma,
|
||||
Period,
|
||||
Slash,
|
||||
BackSlash,
|
||||
Unbound,
|
||||
|
||||
Count,
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ namespace Ryujinx.Graphics.GAL
|
||||
public readonly bool SupportsShaderBallot;
|
||||
public readonly bool SupportsShaderBarrierDivergence;
|
||||
public readonly bool SupportsShaderFloat64;
|
||||
public readonly bool SupportsShaderNonUniformIndexing;
|
||||
public readonly bool SupportsTextureGatherOffsets;
|
||||
public readonly bool SupportsTextureShadowLod;
|
||||
public readonly bool SupportsVertexStoreAndAtomics;
|
||||
@@ -110,6 +111,7 @@ namespace Ryujinx.Graphics.GAL
|
||||
bool supportsShaderBallot,
|
||||
bool supportsShaderBarrierDivergence,
|
||||
bool supportsShaderFloat64,
|
||||
bool supportsShaderNonUniformIndexing,
|
||||
bool supportsTextureGatherOffsets,
|
||||
bool supportsTextureShadowLod,
|
||||
bool supportsVertexStoreAndAtomics,
|
||||
@@ -172,6 +174,7 @@ namespace Ryujinx.Graphics.GAL
|
||||
SupportsShaderBallot = supportsShaderBallot;
|
||||
SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence;
|
||||
SupportsShaderFloat64 = supportsShaderFloat64;
|
||||
SupportsShaderNonUniformIndexing = supportsShaderNonUniformIndexing;
|
||||
SupportsTextureGatherOffsets = supportsTextureGatherOffsets;
|
||||
SupportsTextureShadowLod = supportsTextureShadowLod;
|
||||
SupportsVertexStoreAndAtomics = supportsVertexStoreAndAtomics;
|
||||
|
||||
@@ -49,12 +49,9 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
private const int MinCountForDeletion = 32;
|
||||
private const int MaxCapacity = 2048;
|
||||
private const ulong GiB = 1024 * 1024 * 1024;
|
||||
private ulong MaxTextureSizeCapacity = 4UL * GiB;
|
||||
private ulong MaxTextureSizeCapacity = 2 * GiB;
|
||||
private const ulong MinTextureSizeCapacity = 512 * 1024 * 1024;
|
||||
private const ulong DefaultTextureSizeCapacity = 1 * GiB;
|
||||
private const ulong TextureSizeCapacity6GiB = 4 * GiB;
|
||||
private const ulong TextureSizeCapacity8GiB = 6 * GiB;
|
||||
private const ulong TextureSizeCapacity12GiB = 12 * GiB;
|
||||
|
||||
private const float MemoryScaleFactor = 0.50f;
|
||||
private ulong _maxCacheMemoryUsage = DefaultTextureSizeCapacity;
|
||||
@@ -73,31 +70,22 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
/// <remarks>
|
||||
/// If the backend GPU has 0 memory capacity, the cache size defaults to `DefaultTextureSizeCapacity`.
|
||||
///
|
||||
/// Reads the current Device total CPU Memory, to determine the maximum amount of Vram available. Capped to 50% of Current GPU Memory.
|
||||
/// Reads the current Device total CPU Memory, to determine the maximum amount of VRAM available. Capped to 50% of Current GPU Memory.
|
||||
/// </remarks>
|
||||
/// <param name="context">The GPU context that the cache belongs to</param>
|
||||
/// <param name="cpuMemorySize">The amount of physical CPU Memory Avaiable on the device.</param>
|
||||
/// <param name="cpuMemorySize">The amount of physical CPU Memory available on the device.</param>
|
||||
public void Initialize(GpuContext context, ulong cpuMemorySize)
|
||||
{
|
||||
ulong cpuMemorySizeGiB = cpuMemorySize / GiB;
|
||||
ulong MaximumGpuMemoryGiB = context.Capabilities.MaximumGpuMemory / GiB;
|
||||
ulong TextureSizeCapacity = cpuMemorySize - (2 * GiB);
|
||||
|
||||
if (cpuMemorySizeGiB < 6 || context.Capabilities.MaximumGpuMemory == 0)
|
||||
{
|
||||
_maxCacheMemoryUsage = DefaultTextureSizeCapacity;
|
||||
return;
|
||||
}
|
||||
else if (cpuMemorySizeGiB == 6)
|
||||
{
|
||||
MaxTextureSizeCapacity = TextureSizeCapacity6GiB;
|
||||
}
|
||||
else if (cpuMemorySizeGiB == 8)
|
||||
{
|
||||
MaxTextureSizeCapacity = TextureSizeCapacity8GiB;
|
||||
}
|
||||
else
|
||||
{
|
||||
MaxTextureSizeCapacity = TextureSizeCapacity12GiB;
|
||||
}
|
||||
MaxTextureSizeCapacity =
|
||||
context.Capabilities.MaximumGpuMemory == 0 || cpuMemorySizeGiB < 6 && MaximumGpuMemoryGiB < 6
|
||||
? DefaultTextureSizeCapacity
|
||||
: cpuMemorySizeGiB < 12
|
||||
? TextureSizeCapacity
|
||||
: cpuMemorySize;
|
||||
|
||||
ulong cacheMemory = (ulong)(context.Capabilities.MaximumGpuMemory * MemoryScaleFactor);
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache
|
||||
private const ushort FileFormatVersionMajor = 1;
|
||||
private const ushort FileFormatVersionMinor = 2;
|
||||
private const uint FileFormatVersionPacked = ((uint)FileFormatVersionMajor << 16) | FileFormatVersionMinor;
|
||||
private const uint CodeGenVersion = 7353;
|
||||
private const uint CodeGenVersion = 7354;
|
||||
|
||||
private const string SharedTocFileName = "shared.toc";
|
||||
private const string SharedDataFileName = "shared.data";
|
||||
|
||||
@@ -231,6 +231,8 @@ namespace Ryujinx.Graphics.Gpu.Shader
|
||||
|
||||
public bool QueryHostSupportsShaderFloat64() => _context.Capabilities.SupportsShaderFloat64;
|
||||
|
||||
public bool QueryHostSupportsShaderNonUniformIndexing() => _context.Capabilities.SupportsShaderNonUniformIndexing;
|
||||
|
||||
public bool QueryHostSupportsSnormBufferTextureFormat() => _context.Capabilities.SupportsSnormBufferTextureFormat;
|
||||
|
||||
public bool QueryHostSupportsTextureGatherOffsets() => _context.Capabilities.SupportsTextureGatherOffsets;
|
||||
|
||||
@@ -184,6 +184,7 @@ namespace Ryujinx.Graphics.OpenGL
|
||||
supportsShaderBallot: HwCapabilities.SupportsShaderBallot,
|
||||
supportsShaderBarrierDivergence: !(intelWindows || intelUnix),
|
||||
supportsShaderFloat64: true,
|
||||
supportsShaderNonUniformIndexing: false,
|
||||
supportsTextureGatherOffsets: true,
|
||||
supportsTextureShadowLod: HwCapabilities.SupportsTextureShadowLod,
|
||||
supportsVertexStoreAndAtomics: true,
|
||||
|
||||
@@ -82,6 +82,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
|
||||
public bool IsMainFunction { get; private set; }
|
||||
public bool MayHaveReturned { get; set; }
|
||||
public bool WasNonUniformAccessDeclared { get; set; }
|
||||
|
||||
public CodeGenContext(
|
||||
StructuredProgramInfo info,
|
||||
@@ -89,6 +90,8 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
GeneratorPool<Instruction> instPool,
|
||||
GeneratorPool<LiteralInteger> integerPool) : base(SpirvVersionPacked, instPool, integerPool)
|
||||
{
|
||||
WasNonUniformAccessDeclared = false;
|
||||
|
||||
Info = info;
|
||||
AttributeUsage = parameters.AttributeUsage;
|
||||
Definitions = parameters.Definitions;
|
||||
|
||||
@@ -587,6 +587,23 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
return OperationResult.Invalid;
|
||||
}
|
||||
|
||||
private static void MarkNonUniform(CodeGenContext context, SpvInstruction inst)
|
||||
{
|
||||
if (context.HostCapabilities.SupportsShaderNonUniformIndexing)
|
||||
{
|
||||
if (!context.WasNonUniformAccessDeclared)
|
||||
{
|
||||
context.AddExtension("SPV_EXT_descriptor_indexing");
|
||||
context.AddCapability(Capability.ShaderNonUniform);
|
||||
context.AddCapability(Capability.SampledImageArrayNonUniformIndexing);
|
||||
context.AddCapability(Capability.StorageImageArrayNonUniformIndexing);
|
||||
}
|
||||
|
||||
context.Decorate(inst, Decoration.NonUniform);
|
||||
context.WasNonUniformAccessDeclared = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static OperationResult GenerateImageAtomic(CodeGenContext context, AstOperation operation)
|
||||
{
|
||||
AstTextureOperation texOp = (AstTextureOperation)operation;
|
||||
@@ -613,6 +630,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
SpvInstruction textureIndex = Src(AggregateType.S32);
|
||||
|
||||
image = context.AccessChain(imagePointerType, image, textureIndex);
|
||||
MarkNonUniform(context, image);
|
||||
}
|
||||
|
||||
int coordsCount = texOp.Type.Dimensions;
|
||||
@@ -683,15 +701,21 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
|
||||
ImageDeclaration declaration = context.Images[texOp.GetTextureSetAndBinding()];
|
||||
SpvInstruction image = declaration.Image;
|
||||
bool isIndexed = declaration.IsIndexed;
|
||||
|
||||
if (declaration.IsIndexed)
|
||||
if (isIndexed)
|
||||
{
|
||||
SpvInstruction textureIndex = Src(AggregateType.S32);
|
||||
|
||||
image = context.AccessChain(declaration.ImagePointerType, image, textureIndex);
|
||||
MarkNonUniform(context, image);
|
||||
}
|
||||
|
||||
image = context.Load(declaration.ImageType, image);
|
||||
if (isIndexed)
|
||||
{
|
||||
MarkNonUniform(context, image);
|
||||
}
|
||||
|
||||
int coordsCount = texOp.Type.Dimensions;
|
||||
|
||||
@@ -740,15 +764,21 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
|
||||
ImageDeclaration declaration = context.Images[texOp.GetTextureSetAndBinding()];
|
||||
SpvInstruction image = declaration.Image;
|
||||
bool isIndexed = declaration.IsIndexed;
|
||||
|
||||
if (declaration.IsIndexed)
|
||||
if (isIndexed)
|
||||
{
|
||||
SpvInstruction textureIndex = Src(AggregateType.S32);
|
||||
|
||||
image = context.AccessChain(declaration.ImagePointerType, image, textureIndex);
|
||||
MarkNonUniform(context, image);
|
||||
}
|
||||
|
||||
image = context.Load(declaration.ImageType, image);
|
||||
if (isIndexed)
|
||||
{
|
||||
MarkNonUniform(context, image);
|
||||
}
|
||||
|
||||
int coordsCount = texOp.Type.Dimensions;
|
||||
|
||||
@@ -1878,35 +1908,56 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Spirv
|
||||
private static SpvInstruction GenerateSampledImageLoad(CodeGenContext context, AstTextureOperation texOp, SamplerDeclaration declaration, ref int srcIndex)
|
||||
{
|
||||
SpvInstruction image = declaration.Image;
|
||||
bool imageIndexed = declaration.IsIndexed;
|
||||
|
||||
if (declaration.IsIndexed)
|
||||
if (imageIndexed)
|
||||
{
|
||||
SpvInstruction textureIndex = context.Get(AggregateType.S32, texOp.GetSource(srcIndex++));
|
||||
|
||||
image = context.AccessChain(declaration.SampledImagePointerType, image, textureIndex);
|
||||
MarkNonUniform(context, image);
|
||||
}
|
||||
|
||||
if (texOp.IsSeparate)
|
||||
{
|
||||
image = context.Load(declaration.ImageType, image);
|
||||
if (imageIndexed)
|
||||
{
|
||||
MarkNonUniform(context, image);
|
||||
}
|
||||
|
||||
SamplerDeclaration samplerDeclaration = context.Samplers[texOp.GetSamplerSetAndBinding()];
|
||||
|
||||
SpvInstruction sampler = samplerDeclaration.Image;
|
||||
bool samplerIndexed = samplerDeclaration.IsIndexed;
|
||||
|
||||
if (samplerDeclaration.IsIndexed)
|
||||
if (samplerIndexed)
|
||||
{
|
||||
SpvInstruction samplerIndex = context.Get(AggregateType.S32, texOp.GetSource(srcIndex++));
|
||||
|
||||
sampler = context.AccessChain(samplerDeclaration.SampledImagePointerType, sampler, samplerIndex);
|
||||
MarkNonUniform(context, sampler);
|
||||
}
|
||||
|
||||
sampler = context.Load(samplerDeclaration.ImageType, sampler);
|
||||
if (samplerIndexed)
|
||||
{
|
||||
MarkNonUniform(context, sampler);
|
||||
}
|
||||
|
||||
image = context.SampledImage(declaration.SampledImageType, image, sampler);
|
||||
if (imageIndexed || samplerIndexed)
|
||||
{
|
||||
MarkNonUniform(context, image);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
image = context.Load(declaration.SampledImageType, image);
|
||||
if (imageIndexed)
|
||||
{
|
||||
MarkNonUniform(context, image);
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
|
||||
@@ -336,6 +336,10 @@ namespace Ryujinx.Graphics.Shader
|
||||
{
|
||||
return true;
|
||||
}
|
||||
bool QueryHostSupportsShaderNonUniformIndexing()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Queries host GPU support for signed normalized buffer texture formats.
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace Ryujinx.Graphics.Shader.Translation
|
||||
public readonly bool SupportsShaderBallot;
|
||||
public readonly bool SupportsShaderBarrierDivergence;
|
||||
public readonly bool SupportsShaderFloat64;
|
||||
public readonly bool SupportsShaderNonUniformIndexing;
|
||||
public readonly bool SupportsTextureShadowLod;
|
||||
public readonly bool SupportsViewportMask;
|
||||
|
||||
@@ -20,6 +21,7 @@ namespace Ryujinx.Graphics.Shader.Translation
|
||||
bool supportsShaderBallot,
|
||||
bool supportsShaderBarrierDivergence,
|
||||
bool supportsShaderFloat64,
|
||||
bool supportsShaderNonUniformIndexing,
|
||||
bool supportsTextureShadowLod,
|
||||
bool supportsViewportMask)
|
||||
{
|
||||
@@ -30,6 +32,7 @@ namespace Ryujinx.Graphics.Shader.Translation
|
||||
SupportsShaderBallot = supportsShaderBallot;
|
||||
SupportsShaderBarrierDivergence = supportsShaderBarrierDivergence;
|
||||
SupportsShaderFloat64 = supportsShaderFloat64;
|
||||
SupportsShaderNonUniformIndexing = supportsShaderNonUniformIndexing;
|
||||
SupportsTextureShadowLod = supportsTextureShadowLod;
|
||||
SupportsViewportMask = supportsViewportMask;
|
||||
}
|
||||
|
||||
@@ -364,6 +364,7 @@ namespace Ryujinx.Graphics.Shader.Translation
|
||||
GpuAccessor.QueryHostSupportsShaderBallot(),
|
||||
GpuAccessor.QueryHostSupportsShaderBarrierDivergence(),
|
||||
GpuAccessor.QueryHostSupportsShaderFloat64(),
|
||||
GpuAccessor.QueryHostSupportsShaderNonUniformIndexing(),
|
||||
GpuAccessor.QueryHostSupportsTextureShadowLod(),
|
||||
GpuAccessor.QueryHostSupportsViewportMask());
|
||||
|
||||
|
||||
@@ -46,7 +46,14 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
|
||||
public static (AccessFlags Access, PipelineStageFlags Stages) GetSubpassAccessSuperset(VulkanRenderer gd)
|
||||
{
|
||||
AccessFlags access = BufferAccess;
|
||||
AccessFlags access = BufferAccess |
|
||||
AccessFlags.ShaderReadBit |
|
||||
AccessFlags.ShaderWriteBit |
|
||||
AccessFlags.ColorAttachmentReadBit |
|
||||
AccessFlags.ColorAttachmentWriteBit |
|
||||
AccessFlags.DepthStencilAttachmentReadBit |
|
||||
AccessFlags.DepthStencilAttachmentWriteBit;
|
||||
|
||||
PipelineStageFlags stages = PipelineStageFlags.AllGraphicsBit;
|
||||
|
||||
if (gd.TransformFeedbackApi != null)
|
||||
|
||||
@@ -42,10 +42,11 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
ResourceDescriptor descriptor = rdc.Descriptors[descIndex];
|
||||
ResourceStages stages = descriptor.Stages;
|
||||
|
||||
if (descriptor.Type == ResourceType.StorageBuffer && isMoltenVk)
|
||||
if (descriptor.Type == ResourceType.StorageBuffer && gd.IsMoltenVk)
|
||||
{
|
||||
// There's a bug on MoltenVK where using the same buffer across different stages
|
||||
// There's a bug in MoltenVK where using the same buffer across different stages
|
||||
// causes invalid resource errors, allow the binding on all active stages as workaround.
|
||||
// https://github.com/KhronosGroup/MoltenVK/issues/1870
|
||||
stages = activeStages;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
Image dstImage,
|
||||
TextureCreateInfo srcInfo,
|
||||
TextureCreateInfo dstInfo,
|
||||
TextureCreateInfo srcStorageInfo,
|
||||
TextureCreateInfo dstStorageInfo,
|
||||
Extents2D srcRegion,
|
||||
Extents2D dstRegion,
|
||||
int srcLayer,
|
||||
@@ -40,6 +42,13 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
return (xy1, xy2);
|
||||
}
|
||||
|
||||
static (Offset3D, Offset3D) ClampOffsetsToMip(Offset3D xy1, Offset3D xy2, int mipW, int mipH)
|
||||
{
|
||||
return (
|
||||
new Offset3D(Math.Min(xy1.X, mipW), Math.Min(xy1.Y, mipH), xy1.Z),
|
||||
new Offset3D(Math.Min(xy2.X, mipW), Math.Min(xy2.Y, mipH), xy2.Z));
|
||||
}
|
||||
|
||||
if (srcAspectFlags == 0)
|
||||
{
|
||||
srcAspectFlags = srcInfo.Format.ConvertAspectFlags();
|
||||
@@ -80,6 +89,14 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
(srcOffsets.Element0, srcOffsets.Element1) = ExtentsToOffset3D(srcRegion, srcInfo.Width, srcInfo.Height, level);
|
||||
(dstOffsets.Element0, dstOffsets.Element1) = ExtentsToOffset3D(dstRegion, dstInfo.Width, dstInfo.Height, level);
|
||||
|
||||
int srcMipW = Math.Max(1, srcStorageInfo.Width >> (int)copySrcLevel);
|
||||
int srcMipH = Math.Max(1, srcStorageInfo.Height >> (int)copySrcLevel);
|
||||
int dstMipW = Math.Max(1, dstStorageInfo.Width >> (int)copyDstLevel);
|
||||
int dstMipH = Math.Max(1, dstStorageInfo.Height >> (int)copyDstLevel);
|
||||
|
||||
(srcOffsets.Element0, srcOffsets.Element1) = ClampOffsetsToMip(srcOffsets.Element0, srcOffsets.Element1, srcMipW, srcMipH);
|
||||
(dstOffsets.Element0, dstOffsets.Element1) = ClampOffsetsToMip(dstOffsets.Element0, dstOffsets.Element1, dstMipW, dstMipH);
|
||||
|
||||
ImageBlit region = new()
|
||||
{
|
||||
SrcSubresource = srcSl,
|
||||
@@ -121,6 +138,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
Image dstImage,
|
||||
TextureCreateInfo srcInfo,
|
||||
TextureCreateInfo dstInfo,
|
||||
TextureCreateInfo srcStorageInfo,
|
||||
TextureCreateInfo dstStorageInfo,
|
||||
int srcViewLayer,
|
||||
int dstViewLayer,
|
||||
int srcViewLevel,
|
||||
@@ -151,6 +170,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
dstImage,
|
||||
srcInfo,
|
||||
dstInfo,
|
||||
srcStorageInfo,
|
||||
dstStorageInfo,
|
||||
srcViewLayer,
|
||||
dstViewLayer,
|
||||
srcViewLevel,
|
||||
@@ -186,6 +207,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
Image dstImage,
|
||||
TextureCreateInfo srcInfo,
|
||||
TextureCreateInfo dstInfo,
|
||||
TextureCreateInfo srcStorageInfo,
|
||||
TextureCreateInfo dstStorageInfo,
|
||||
int srcViewLayer,
|
||||
int dstViewLayer,
|
||||
int srcViewLevel,
|
||||
@@ -314,6 +337,14 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
int copyWidth = sizeInBlocks ? BitUtils.DivRoundUp(width, blockWidth) : width;
|
||||
int copyHeight = sizeInBlocks ? BitUtils.DivRoundUp(height, blockHeight) : height;
|
||||
|
||||
int srcMipW = Math.Max(1, srcStorageInfo.Width >> (srcViewLevel + srcLevel + level));
|
||||
int srcMipH = Math.Max(1, srcStorageInfo.Height >> (srcViewLevel + srcLevel + level));
|
||||
int dstMipW = Math.Max(1, dstStorageInfo.Width >> (dstViewLevel + dstLevel + level));
|
||||
int dstMipH = Math.Max(1, dstStorageInfo.Height >> (dstViewLevel + dstLevel + level));
|
||||
|
||||
copyWidth = Math.Min(copyWidth, Math.Min(srcMipW, dstMipW));
|
||||
copyHeight = Math.Min(copyHeight, Math.Min(srcMipH, dstMipH));
|
||||
|
||||
Extent3D extent = new((uint)copyWidth, (uint)copyHeight, (uint)srcDepth);
|
||||
|
||||
if (srcInfo.Samples > 1 && srcInfo.Samples != dstInfo.Samples)
|
||||
|
||||
@@ -67,6 +67,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
|
||||
public VkFormat VkFormat { get; }
|
||||
|
||||
public ImageUsageFlags UsageFlags { get; }
|
||||
|
||||
public unsafe TextureStorage(
|
||||
VulkanRenderer gd,
|
||||
Device device,
|
||||
@@ -93,7 +95,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
|
||||
SampleCountFlags sampleCountFlags = ConvertToSampleCountFlags(gd.Capabilities.SupportedSampleCounts, (uint)info.Samples);
|
||||
|
||||
ImageUsageFlags usage = GetImageUsage(info.Format, gd.Capabilities, isMsImageStorageSupported, true);
|
||||
ImageUsageFlags usage = GetImageUsage(info.Format, gd.Capabilities, isMsImageStorageSupported);
|
||||
UsageFlags = usage;
|
||||
|
||||
ImageCreateFlags flags = ImageCreateFlags.CreateMutableFormatBit | ImageCreateFlags.CreateExtendedUsageBit;
|
||||
|
||||
@@ -159,7 +162,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
|
||||
_imageAuto = new Auto<DisposableImage>(new DisposableImage(_gd.Api, device, _image));
|
||||
|
||||
InitialTransition(ImageLayout.Preinitialized, ImageLayout.General);
|
||||
InitialTransition(ImageLayout.Undefined, ImageLayout.General);
|
||||
}
|
||||
|
||||
_slices = new TextureSliceInfo[levels * _depthOrLayers];
|
||||
@@ -307,7 +310,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
}
|
||||
}
|
||||
|
||||
public static ImageUsageFlags GetImageUsage(Format format, in HardwareCapabilities capabilities, bool isMsImageStorageSupported, bool extendedUsage)
|
||||
public static ImageUsageFlags GetImageUsage(Format format, in HardwareCapabilities capabilities, bool isMsImageStorageSupported)
|
||||
{
|
||||
ImageUsageFlags usage = DefaultUsageFlags;
|
||||
|
||||
@@ -320,7 +323,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
usage |= ImageUsageFlags.ColorAttachmentBit;
|
||||
}
|
||||
|
||||
if ((format.IsImageCompatible && isMsImageStorageSupported) || extendedUsage)
|
||||
if (format.IsImageCompatible && isMsImageStorageSupported)
|
||||
{
|
||||
usage |= ImageUsageFlags.StorageBit;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
bool isMsImageStorageSupported = gd.Capabilities.SupportsShaderStorageImageMultisample || !info.Target.IsMultisample;
|
||||
|
||||
VkFormat format = _gd.FormatCapabilities.ConvertToVkFormat(info.Format, isMsImageStorageSupported);
|
||||
ImageUsageFlags usage = TextureStorage.GetImageUsage(info.Format, gd.Capabilities, isMsImageStorageSupported, false);
|
||||
ImageUsageFlags usage = TextureStorage.GetImageUsage(info.Format, gd.Capabilities, isMsImageStorageSupported) & storage.UsageFlags;
|
||||
|
||||
uint levels = (uint)info.Levels;
|
||||
uint layers = (uint)info.GetLayers();
|
||||
@@ -133,6 +133,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
shaderUsage |= ImageUsageFlags.StorageBit;
|
||||
}
|
||||
|
||||
shaderUsage &= storage.UsageFlags;
|
||||
|
||||
_imageView = CreateImageView(componentMapping, subresourceRange, type, shaderUsage);
|
||||
|
||||
// Framebuffer attachments and storage images requires a identity component mapping.
|
||||
@@ -257,6 +259,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
dstImage,
|
||||
src.Info,
|
||||
dst.Info,
|
||||
src.Storage.Info,
|
||||
dst.Storage.Info,
|
||||
src.FirstLayer,
|
||||
dst.FirstLayer,
|
||||
src.FirstLevel,
|
||||
@@ -310,6 +314,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
dstImage,
|
||||
src.Info,
|
||||
dst.Info,
|
||||
src.Storage.Info,
|
||||
dst.Storage.Info,
|
||||
src.FirstLayer,
|
||||
dst.FirstLayer,
|
||||
src.FirstLevel,
|
||||
@@ -385,6 +391,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
dst.GetImage().Get(cbs).Value,
|
||||
src.Info,
|
||||
dst.Info,
|
||||
src.Storage.Info,
|
||||
dst.Storage.Info,
|
||||
src.FirstLayer,
|
||||
dst.FirstLayer,
|
||||
src.FirstLevel,
|
||||
@@ -410,6 +418,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
dst.GetImage().Get(cbs).Value,
|
||||
src.Info,
|
||||
dst.Info,
|
||||
src.Storage.Info,
|
||||
dst.Storage.Info,
|
||||
srcRegion,
|
||||
dstRegion,
|
||||
src.FirstLayer,
|
||||
@@ -463,6 +473,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
dstImage.Get(cbs).Value,
|
||||
src.Info,
|
||||
dst.Info,
|
||||
src.Storage.Info,
|
||||
dst.Storage.Info,
|
||||
srcRegion,
|
||||
dstRegion,
|
||||
src.FirstLayer,
|
||||
|
||||
@@ -68,9 +68,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
int stride = (_stride + (alignment - 1)) & -alignment;
|
||||
int newSize = (_size / _stride) * stride;
|
||||
|
||||
Buffer buffer = autoBuffer.Get(cbs, 0, newSize).Value;
|
||||
|
||||
updater.BindVertexBuffer(cbs, binding, buffer, 0, (ulong)newSize, (ulong)stride);
|
||||
updater.BindVertexBuffer(cbs, binding, autoBuffer, 0, newSize, (ulong)stride);
|
||||
|
||||
_buffer = autoBuffer;
|
||||
|
||||
@@ -93,11 +91,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
|
||||
if (autoBuffer != null)
|
||||
{
|
||||
int offset = _offset;
|
||||
bool mirrorable = _size <= VertexBufferMaxMirrorable;
|
||||
Buffer buffer = mirrorable ? autoBuffer.GetMirrorable(cbs, ref offset, _size, out _).Value : autoBuffer.Get(cbs, offset, _size).Value;
|
||||
|
||||
updater.BindVertexBuffer(cbs, binding, buffer, (ulong)offset, (ulong)_size, (ulong)_stride);
|
||||
updater.BindVertexBuffer(cbs, binding, autoBuffer, _offset, _size, (ulong)_stride);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using VkBuffer = Silk.NET.Vulkan.Buffer;
|
||||
|
||||
namespace Ryujinx.Graphics.Vulkan
|
||||
@@ -15,6 +16,10 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
private readonly NativeArray<ulong> _sizes;
|
||||
private readonly NativeArray<ulong> _strides;
|
||||
|
||||
private readonly Auto<DisposableBuffer>[] _bufferAutos;
|
||||
private readonly int[] _bufferOffsetsForGet;
|
||||
private readonly int[] _bufferSizesForGet;
|
||||
|
||||
public VertexBufferUpdater(VulkanRenderer gd)
|
||||
{
|
||||
_gd = gd;
|
||||
@@ -23,9 +28,13 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
_offsets = new NativeArray<ulong>(Constants.MaxVertexBuffers);
|
||||
_sizes = new NativeArray<ulong>(Constants.MaxVertexBuffers);
|
||||
_strides = new NativeArray<ulong>(Constants.MaxVertexBuffers);
|
||||
|
||||
_bufferAutos = new Auto<DisposableBuffer>[Constants.MaxVertexBuffers];
|
||||
_bufferOffsetsForGet = new int[Constants.MaxVertexBuffers];
|
||||
_bufferSizesForGet = new int[Constants.MaxVertexBuffers];
|
||||
}
|
||||
|
||||
public void BindVertexBuffer(CommandBufferScoped cbs, uint binding, VkBuffer buffer, ulong offset, ulong size, ulong stride)
|
||||
public void BindVertexBuffer(CommandBufferScoped cbs, uint binding, Auto<DisposableBuffer> autoBuffer, int offset, int size, ulong stride)
|
||||
{
|
||||
if (_count == 0)
|
||||
{
|
||||
@@ -39,9 +48,11 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
|
||||
int index = (int)_count;
|
||||
|
||||
_buffers[index] = buffer;
|
||||
_offsets[index] = offset;
|
||||
_sizes[index] = size;
|
||||
_bufferAutos[index] = autoBuffer;
|
||||
_bufferOffsetsForGet[index] = offset;
|
||||
_bufferSizesForGet[index] = size;
|
||||
_offsets[index] = (ulong)offset;
|
||||
_sizes[index] = (ulong)size;
|
||||
_strides[index] = stride;
|
||||
|
||||
_count++;
|
||||
@@ -51,23 +62,65 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
{
|
||||
if (_count != 0)
|
||||
{
|
||||
if (_gd.Capabilities.SupportsExtendedDynamicState)
|
||||
int count = (int)_count;
|
||||
uint baseBinding = _baseBinding;
|
||||
_count = 0;
|
||||
|
||||
Auto<DisposableBuffer>[] autos = ArrayPool<Auto<DisposableBuffer>>.Shared.Rent(count);
|
||||
Span<int> getOffsets = stackalloc int[Constants.MaxVertexBuffers];
|
||||
Span<int> getSizes = stackalloc int[Constants.MaxVertexBuffers];
|
||||
Span<ulong> offsets = stackalloc ulong[Constants.MaxVertexBuffers];
|
||||
Span<ulong> sizes = stackalloc ulong[Constants.MaxVertexBuffers];
|
||||
Span<ulong> strides = stackalloc ulong[Constants.MaxVertexBuffers];
|
||||
Span<VkBuffer> buffers = stackalloc VkBuffer[Constants.MaxVertexBuffers];
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
_gd.ExtendedDynamicStateApi.CmdBindVertexBuffers2(
|
||||
cbs.CommandBuffer,
|
||||
_baseBinding,
|
||||
_count,
|
||||
_buffers.Pointer,
|
||||
_offsets.Pointer,
|
||||
_sizes.Pointer,
|
||||
_strides.Pointer);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gd.Api.CmdBindVertexBuffers(cbs.CommandBuffer, _baseBinding, _count, _buffers.Pointer, _offsets.Pointer);
|
||||
autos[i] = _bufferAutos[i];
|
||||
_bufferAutos[i] = null;
|
||||
getOffsets[i] = _bufferOffsetsForGet[i];
|
||||
getSizes[i] = _bufferSizesForGet[i];
|
||||
offsets[i] = _offsets[i];
|
||||
sizes[i] = _sizes[i];
|
||||
strides[i] = _strides[i];
|
||||
}
|
||||
|
||||
_count = 0;
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
buffers[i] = autos[i].Get(cbs, getOffsets[i], getSizes[i]).Value;
|
||||
autos[i] = null;
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
_buffers[i] = buffers[i];
|
||||
_offsets[i] = offsets[i];
|
||||
_sizes[i] = sizes[i];
|
||||
_strides[i] = strides[i];
|
||||
}
|
||||
|
||||
if (_gd.Capabilities.SupportsExtendedDynamicState)
|
||||
{
|
||||
_gd.ExtendedDynamicStateApi.CmdBindVertexBuffers2(
|
||||
cbs.CommandBuffer,
|
||||
baseBinding,
|
||||
(uint)count,
|
||||
_buffers.Pointer,
|
||||
_offsets.Pointer,
|
||||
_sizes.Pointer,
|
||||
_strides.Pointer);
|
||||
}
|
||||
else
|
||||
{
|
||||
_gd.Api.CmdBindVertexBuffers(cbs.CommandBuffer, baseBinding, (uint)count, _buffers.Pointer, _offsets.Pointer);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<Auto<DisposableBuffer>>.Shared.Return(autos, clearArray: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -494,6 +494,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
UniformBufferStandardLayout = supportedPhysicalDeviceVulkan12Features.UniformBufferStandardLayout,
|
||||
UniformAndStorageBuffer8BitAccess = supportedPhysicalDeviceVulkan12Features.UniformAndStorageBuffer8BitAccess,
|
||||
StorageBuffer8BitAccess = supportedPhysicalDeviceVulkan12Features.StorageBuffer8BitAccess,
|
||||
ShaderSampledImageArrayNonUniformIndexing = supportedPhysicalDeviceVulkan12Features.ShaderSampledImageArrayNonUniformIndexing,
|
||||
ShaderStorageImageArrayNonUniformIndexing = supportedPhysicalDeviceVulkan12Features.ShaderStorageImageArrayNonUniformIndexing,
|
||||
};
|
||||
|
||||
pExtendedFeatures = &featuresVk12;
|
||||
|
||||
@@ -435,8 +435,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
_physicalDevice.IsDeviceExtensionPresent(ExtExtendedDynamicState.ExtensionName),
|
||||
features2.Features.MultiViewport && !(IsMoltenVk && Vendor == Vendor.Amd), // Workaround for AMD on MoltenVK issue
|
||||
featuresRobustness2.NullDescriptor || IsMoltenVk,
|
||||
supportsPushDescriptors && !IsMoltenVk,
|
||||
propertiesPushDescriptor.MaxPushDescriptors,
|
||||
supportsPushDescriptors,
|
||||
IsMoltenVk ? 16 : propertiesPushDescriptor.MaxPushDescriptors, // In case an old version of MoltenVK is used, apply a limit to prevent vertex explosions.
|
||||
featuresPrimitiveTopologyListRestart.PrimitiveTopologyListRestart,
|
||||
featuresPrimitiveTopologyListRestart.PrimitiveTopologyPatchListRestart,
|
||||
supportsTransformFeedback,
|
||||
@@ -775,7 +775,11 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
supportsShaderBallot: false,
|
||||
supportsShaderBarrierDivergence: Vendor != Vendor.Intel,
|
||||
supportsShaderFloat64: Capabilities.SupportsShaderFloat64,
|
||||
supportsTextureGatherOffsets: features2.Features.ShaderImageGatherExtended && !IsMoltenVk,
|
||||
|
||||
supportsShaderNonUniformIndexing:
|
||||
featuresVk12.ShaderSampledImageArrayNonUniformIndexing &&
|
||||
featuresVk12.ShaderStorageImageArrayNonUniformIndexing,
|
||||
supportsTextureGatherOffsets: features2.Features.ShaderImageGatherExtended,
|
||||
supportsTextureShadowLod: false,
|
||||
supportsVertexStoreAndAtomics: features2.Features.VertexPipelineStoresAndAtomics,
|
||||
supportsViewportIndexVertexTessellation: featuresVk12.ShaderOutputViewportIndex,
|
||||
|
||||
@@ -391,12 +391,12 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
{
|
||||
if (_effect != null)
|
||||
{
|
||||
_gd.FlushAllCommands();
|
||||
_gd.CommandBufferPool.Return(
|
||||
cbs,
|
||||
null,
|
||||
[PipelineStageFlags.ColorAttachmentOutputBit],
|
||||
null);
|
||||
_gd.FlushAllCommands();
|
||||
cbs.GetFence().Wait();
|
||||
cbs = _gd.CommandBufferPool.Rent();
|
||||
}
|
||||
@@ -455,6 +455,8 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
ImageLayout.General,
|
||||
ImageLayout.PresentSrcKhr);
|
||||
|
||||
_gd.FlushAllCommands();
|
||||
|
||||
_gd.CommandBufferPool.Return(
|
||||
cbs,
|
||||
[_imageAvailableSemaphores[semaphoreIndex]],
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
|
||||
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" >
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace Ryujinx.HLE.HOS.Applets
|
||||
case AppletId.LibAppletOff:
|
||||
return new BrowserApplet();
|
||||
case AppletId.MiiEdit:
|
||||
Logger.Warning?.Print(LogClass.Application, $"Please use the MiiEdit inside File/Open Applet");
|
||||
Logger.Warning?.Print(LogClass.Application, $"Please use the Mii Editor inside Actions/Tools");
|
||||
return new DummyApplet(system);
|
||||
case AppletId.Cabinet:
|
||||
return new CabinetApplet(system);
|
||||
|
||||
@@ -118,7 +118,9 @@ namespace Ryujinx.HLE.HOS
|
||||
}
|
||||
}
|
||||
|
||||
DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize, _diskCacheSelector ?? "default");
|
||||
string cacheSelector = _diskCacheSelector ?? "default";
|
||||
|
||||
DiskCacheLoadState = processContext.Initialize(_titleIdText, _displayVersion, _diskCacheEnabled, _codeAddress, _codeSize, cacheSelector);
|
||||
|
||||
return processContext;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace Ryujinx.HLE.HOS
|
||||
public IpcMessage Response { get; }
|
||||
public BinaryReader RequestData { get; }
|
||||
public BinaryWriter ResponseData { get; }
|
||||
public ulong ClientProcessId => Request.HandleDesc is { HasPId: true } ? Request.HandleDesc.PId : Process.Pid;
|
||||
|
||||
public ServiceCtx(
|
||||
Switch device,
|
||||
|
||||
@@ -190,7 +190,7 @@ namespace Ryujinx.HLE.HOS.Services.Account.Acc
|
||||
// TODO: Account actually calls nn::arp::detail::IReader::GetApplicationControlProperty() with the current Pid and store the result (NACP file) internally.
|
||||
// But since we use LibHac and we load one Application at a time, it's not necessary.
|
||||
|
||||
context.ResponseData.Write((byte)context.Device.Processes.ActiveApplication.ApplicationControlProperties.UserAccountSwitchLock);
|
||||
context.ResponseData.Write((byte)context.Device.Processes.GetProcess(context.ClientProcessId).ApplicationControlProperties.UserAccountSwitchLock);
|
||||
|
||||
Logger.Stub?.PrintStub(LogClass.ServiceAcc);
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService
|
||||
// OpenLibraryAppletSelfAccessor() -> object<nn::am::service::ILibraryAppletSelfAccessor>
|
||||
public ResultCode OpenLibraryAppletSelfAccessor(ServiceCtx context)
|
||||
{
|
||||
MakeObject(context, new ILibraryAppletSelfAccessor(context));
|
||||
MakeObject(context, new ILibraryAppletSelfAccessor(context, _pid));
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.LibraryAppletProxy
|
||||
@@ -7,9 +8,11 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
|
||||
{
|
||||
private readonly AppletStandalone _appletStandalone = new();
|
||||
|
||||
public ILibraryAppletSelfAccessor(ServiceCtx context)
|
||||
public ILibraryAppletSelfAccessor(ServiceCtx context, ulong pid)
|
||||
{
|
||||
if (context.Device.Processes.ActiveApplication.ProgramId == 0x0100000000001009)
|
||||
ulong programId = context.Device.Processes.GetProcess(pid).ProgramId;
|
||||
|
||||
if (programId == 0x0100000000001009)
|
||||
{
|
||||
// Create MiiEdit data.
|
||||
_appletStandalone = new AppletStandalone()
|
||||
@@ -25,7 +28,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException($"{context.Device.Processes.ActiveApplication.ProgramId} applet is not implemented.");
|
||||
throw new NotImplementedException($"{programId} applet is not implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +64,19 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandCmif(10)]
|
||||
// ExitProcessAndReturn -> nn::am::service::LibraryAppletInfo
|
||||
public ResultCode ExitProcessAndReturn(ServiceCtx context)
|
||||
{
|
||||
// Exits the LibraryApplet and returns to running the title which launched this LibraryApplet (qlaunch for example).
|
||||
// On success, official sw will enter an infinite loop with sleep-thread value 86400000000000.
|
||||
// Since we don't currently support qlaunch, it's fine to stub it.
|
||||
|
||||
Logger.Stub?.PrintStub(LogClass.Service);
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
|
||||
[CommandCmif(11)]
|
||||
// GetLibraryAppletInfo() -> nn::am::service::LibraryAppletInfo
|
||||
public ResultCode GetLibraryAppletInfo(ServiceCtx context)
|
||||
@@ -83,7 +99,8 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletAE.AllSystemAppletProxiesService.Lib
|
||||
AppletIdentifyInfo appletIdentifyInfo = new()
|
||||
{
|
||||
AppletId = AppletId.QLaunch,
|
||||
TitleId = 0x0100000000001000,
|
||||
// 0x4 padding
|
||||
TitleId = 0x0100000000001000, // qlaunch systemAppletMenu title ID
|
||||
};
|
||||
|
||||
context.ResponseData.WriteStruct(appletIdentifyInfo);
|
||||
|
||||
@@ -44,8 +44,9 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
||||
private int _jitLoaded;
|
||||
|
||||
private readonly LibHac.HorizonClient _horizon;
|
||||
private readonly ulong _pid;
|
||||
|
||||
public IApplicationFunctions(Horizon system)
|
||||
public IApplicationFunctions(Horizon system, ulong pid)
|
||||
{
|
||||
// TODO: Find where they are signaled.
|
||||
_gpuErrorDetectedSystemEvent = new KEvent(system.KernelContext);
|
||||
@@ -55,6 +56,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
||||
_unknownEvent = new KEvent(system.KernelContext);
|
||||
|
||||
_horizon = system.LibHacHorizonManager.AmClient;
|
||||
_pid = pid;
|
||||
}
|
||||
|
||||
[CommandCmif(1)]
|
||||
@@ -115,11 +117,12 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
||||
public ResultCode EnsureSaveData(ServiceCtx context)
|
||||
{
|
||||
Uid userId = context.RequestData.ReadStruct<AccountUid>().ToLibHacUid();
|
||||
var process = context.Device.Processes.GetProcess(_pid);
|
||||
|
||||
// Mask out the low nibble of the program ID to get the application ID
|
||||
ApplicationId applicationId = new(context.Device.Processes.ActiveApplication.ProgramId & ~0xFul);
|
||||
ApplicationId applicationId = new(process.Identity.ApplicationId);
|
||||
|
||||
ApplicationControlProperty nacp = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
|
||||
ApplicationControlProperty nacp = process.ApplicationControlProperties;
|
||||
|
||||
LibHac.HorizonClient hos = context.Device.System.LibHacHorizonManager.AmClient;
|
||||
LibHac.Result result = hos.Fs.EnsureApplicationSaveData(out long requiredSize, applicationId, in nacp, in userId);
|
||||
@@ -139,7 +142,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
||||
// TODO: When above calls are implemented, switch to using ns:am
|
||||
|
||||
long desiredLanguageCode = context.Device.System.State.DesiredLanguageCode;
|
||||
int supportedLanguages = (int)context.Device.Processes.ActiveApplication.ApplicationControlProperties.SupportedLanguageFlag;
|
||||
int supportedLanguages = (int)context.Device.Processes.GetProcess(_pid).ApplicationControlProperties.SupportedLanguageFlag;
|
||||
int firstSupported = BitOperations.TrailingZeroCount(supportedLanguages);
|
||||
|
||||
if (firstSupported > (int)TitleLanguage.BrazilianPortuguese)
|
||||
@@ -182,7 +185,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
||||
public ResultCode GetDisplayVersion(ServiceCtx context)
|
||||
{
|
||||
// If an NACP isn't found, the buffer will be all '\0' which seems to be the correct implementation.
|
||||
context.ResponseData.Write(context.Device.Processes.ActiveApplication.ApplicationControlProperties.DisplayVersion);
|
||||
context.ResponseData.Write(context.Device.Processes.GetProcess(_pid).ApplicationControlProperties.DisplayVersion);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
@@ -235,11 +238,12 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.Applicati
|
||||
ushort index = (ushort)context.RequestData.ReadUInt64();
|
||||
long saveSize = context.RequestData.ReadInt64();
|
||||
long journalSize = context.RequestData.ReadInt64();
|
||||
var process = context.Device.Processes.GetProcess(_pid);
|
||||
|
||||
// Mask out the low nibble of the program ID to get the application ID
|
||||
ApplicationId applicationId = new(context.Device.Processes.ActiveApplication.ProgramId & ~0xFul);
|
||||
ApplicationId applicationId = new(process.Identity.ApplicationId);
|
||||
|
||||
ApplicationControlProperty nacp = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
|
||||
ApplicationControlProperty nacp = process.ApplicationControlProperties;
|
||||
|
||||
LibHac.Result result = _horizon.Fs.CreateApplicationCacheStorage(out long requiredSize,
|
||||
out CacheStorageTargetMedia storageTarget, applicationId, in nacp, index, saveSize, journalSize);
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService
|
||||
// GetApplicationFunctions() -> object<nn::am::service::IApplicationFunctions>
|
||||
public ResultCode GetApplicationFunctions(ServiceCtx context)
|
||||
{
|
||||
MakeObject(context, new IApplicationFunctions(context.Device.System));
|
||||
MakeObject(context, new IApplicationFunctions(context.Device.System, _pid));
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
@@ -27,13 +27,18 @@ namespace Ryujinx.HLE.HOS.Services.Arp
|
||||
}
|
||||
|
||||
public static ApplicationLaunchProperty GetByPid(ServiceCtx context)
|
||||
{
|
||||
return GetByPid(context, context.ClientProcessId);
|
||||
}
|
||||
|
||||
public static ApplicationLaunchProperty GetByPid(ServiceCtx context, ulong pid)
|
||||
{
|
||||
// TODO: Handle ApplicationLaunchProperty as array when pid will be supported and return the right item.
|
||||
// For now we can hardcode values, and fix it after GetApplicationLaunchProperty is implemented.
|
||||
|
||||
return new ApplicationLaunchProperty
|
||||
{
|
||||
TitleId = context.Device.Processes.ActiveApplication.ProgramId,
|
||||
TitleId = context.Device.Processes.GetProcess(pid).ProgramId,
|
||||
Version = 0x00,
|
||||
BaseGameStorageId = (byte)StorageId.BuiltInSystem,
|
||||
UpdateGameStorageId = (byte)StorageId.None,
|
||||
|
||||
@@ -133,9 +133,9 @@ namespace Ryujinx.HLE.HOS.Services.Caps
|
||||
|
||||
using SKBitmap bitmap = new(new SKImageInfo(ScreenshotWidth, ScreenshotHeight, SKColorType.Rgba8888));
|
||||
|
||||
IntPtr pixels = bitmap.GetPixels();
|
||||
nint pixels = bitmap.GetPixels();
|
||||
|
||||
if (pixels == IntPtr.Zero)
|
||||
if (pixels == 0)
|
||||
{
|
||||
return ResultCode.InvalidArgument;
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Ryujinx.HLE.HOS.Services.Caps
|
||||
|
||||
byte[] screenshotData = context.Memory.GetSpan(screenshotDataPosition, (int)screenshotDataSize, true).ToArray();
|
||||
|
||||
ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Processes.ActiveApplication.ProgramId, out ApplicationAlbumEntry applicationAlbumEntry);
|
||||
ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Processes.GetProcess(context.ClientProcessId).ProgramId, out ApplicationAlbumEntry applicationAlbumEntry);
|
||||
|
||||
context.ResponseData.WriteStruct(applicationAlbumEntry);
|
||||
|
||||
@@ -98,7 +98,7 @@ namespace Ryujinx.HLE.HOS.Services.Caps
|
||||
|
||||
byte[] screenshotData = context.Memory.GetSpan(screenshotDataPosition, (int)screenshotDataSize, true).ToArray();
|
||||
|
||||
ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Processes.ActiveApplication.ProgramId, out ApplicationAlbumEntry applicationAlbumEntry);
|
||||
ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Processes.GetProcess(context.ClientProcessId).ProgramId, out ApplicationAlbumEntry applicationAlbumEntry);
|
||||
|
||||
context.ResponseData.WriteStruct(applicationAlbumEntry);
|
||||
|
||||
@@ -143,7 +143,7 @@ namespace Ryujinx.HLE.HOS.Services.Caps
|
||||
|
||||
byte[] screenshotData = context.Memory.GetSpan(screenshotDataPosition, (int)screenshotDataSize, true).ToArray();
|
||||
|
||||
ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Processes.ActiveApplication.ProgramId, out ApplicationAlbumEntry applicationAlbumEntry);
|
||||
ResultCode resultCode = context.Device.System.CaptureManager.SaveScreenShot(screenshotData, appletResourceUserId, context.Device.Processes.GetProcess(context.ClientProcessId).ProgramId, out ApplicationAlbumEntry applicationAlbumEntry);
|
||||
|
||||
context.ResponseData.WriteStruct(applicationAlbumEntry);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.HOS.Services.Fatal.Types;
|
||||
using Ryujinx.HLE.Loaders.Processes;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
@@ -50,12 +51,13 @@ namespace Ryujinx.HLE.HOS.Services.Fatal
|
||||
|
||||
private ResultCode ThrowFatalWithCpuContextImpl(ServiceCtx context, ResultCode resultCode, ulong pid, FatalPolicy fatalPolicy, ReadOnlySpan<byte> cpuContext)
|
||||
{
|
||||
ProcessResult process = context.Device.Processes.GetProcess(pid);
|
||||
StringBuilder errorReport = new();
|
||||
|
||||
errorReport.AppendLine();
|
||||
errorReport.AppendLine("ErrorReport log:");
|
||||
|
||||
errorReport.AppendLine($"\tTitleId: {context.Device.Processes.ActiveApplication.ProgramIdText}");
|
||||
errorReport.AppendLine($"\tTitleId: {process.ProgramIdText}");
|
||||
errorReport.AppendLine($"\tPid: {pid}");
|
||||
errorReport.AppendLine($"\tResultCode: {((int)resultCode & 0x1FF) + 2000}-{((int)resultCode >> 9) & 0x3FFF:d4}");
|
||||
errorReport.AppendLine($"\tFatalPolicy: {fatalPolicy}");
|
||||
@@ -64,7 +66,7 @@ namespace Ryujinx.HLE.HOS.Services.Fatal
|
||||
{
|
||||
errorReport.AppendLine("CPU Context:");
|
||||
|
||||
if (context.Device.Processes.ActiveApplication.Is64Bit)
|
||||
if (process.Is64Bit)
|
||||
{
|
||||
CpuContext64 cpuContext64 = MemoryMarshal.Cast<byte, CpuContext64>(cpuContext)[0];
|
||||
|
||||
|
||||
@@ -885,7 +885,7 @@ namespace Ryujinx.HLE.HOS.Services.Fs
|
||||
{
|
||||
byte programIndex = context.RequestData.ReadByte();
|
||||
|
||||
if ((context.Device.Processes.ActiveApplication.ProgramId & 0xf) != programIndex)
|
||||
if (context.Device.Processes.GetProcess(_pid).Identity.ProgramIndex != programIndex)
|
||||
{
|
||||
throw new NotImplementedException($"Accessing storage from other programs is not supported (program index = {programIndex}).");
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
private bool CheckLocalCommunicationIdPermission(ServiceCtx context, ulong localCommunicationIdChecked)
|
||||
{
|
||||
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
|
||||
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
|
||||
ApplicationControlProperty controlProperty = context.Device.Processes.GetProcess(context.ClientProcessId).ApplicationControlProperties;
|
||||
|
||||
foreach (ulong localCommunicationId in controlProperty.LocalCommunicationId)
|
||||
{
|
||||
@@ -438,7 +438,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
if (scanFilter.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
|
||||
{
|
||||
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
|
||||
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
|
||||
ApplicationControlProperty controlProperty = context.Device.Processes.GetProcess(context.ClientProcessId).ApplicationControlProperties;
|
||||
|
||||
scanFilter.NetworkId.IntentId.LocalCommunicationId = (long)controlProperty.LocalCommunicationId[0];
|
||||
}
|
||||
@@ -613,7 +613,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
if (networkConfig.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
|
||||
{
|
||||
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
|
||||
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
|
||||
ApplicationControlProperty controlProperty = context.Device.Processes.GetProcess(context.ClientProcessId).ApplicationControlProperties;
|
||||
|
||||
networkConfig.IntentId.LocalCommunicationId = (long)controlProperty.LocalCommunicationId[0];
|
||||
}
|
||||
@@ -948,7 +948,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
if (networkInfo.NetworkId.IntentId.LocalCommunicationId == -1 && NetworkClient.NeedsRealId)
|
||||
{
|
||||
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented.
|
||||
ApplicationControlProperty controlProperty = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
|
||||
ApplicationControlProperty controlProperty = context.Device.Processes.GetProcess(context.ClientProcessId).ApplicationControlProperties;
|
||||
|
||||
networkInfo.NetworkId.IntentId.LocalCommunicationId = (long)controlProperty.LocalCommunicationId[0];
|
||||
}
|
||||
@@ -1208,7 +1208,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
}
|
||||
|
||||
// TODO: Call nn::arp::GetApplicationLaunchProperty here when implemented.
|
||||
NetworkClient.SetGameVersion(context.Device.Processes.ActiveApplication.ApplicationControlProperties.DisplayVersion);
|
||||
NetworkClient.SetGameVersion(context.Device.Processes.GetProcess(context.ClientProcessId).ApplicationControlProperties.DisplayVersion);
|
||||
|
||||
resultCode = ResultCode.Success;
|
||||
_nifmResultCode = resultCode;
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
|
||||
|
||||
// NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId.
|
||||
|
||||
return CountAddOnContentImpl(context, context.Device.Processes.ActiveApplication.ProgramId);
|
||||
return CountAddOnContentImpl(context, context.Device.Processes.GetProcess(pid).ProgramId);
|
||||
}
|
||||
|
||||
[CommandCmif(3)]
|
||||
@@ -63,7 +63,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
|
||||
|
||||
// NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId.
|
||||
|
||||
return ListAddContentImpl(context, context.Device.Processes.ActiveApplication.ProgramId);
|
||||
return ListAddContentImpl(context, context.Device.Processes.GetProcess(pid).ProgramId);
|
||||
}
|
||||
|
||||
[CommandCmif(4)] // 1.0.0-6.2.0
|
||||
@@ -85,7 +85,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
|
||||
|
||||
// NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId.
|
||||
|
||||
return GetAddOnContentBaseIdImpl(context, context.Device.Processes.ActiveApplication.ProgramId);
|
||||
return GetAddOnContentBaseIdImpl(context, context.Device.Processes.GetProcess(pid).ProgramId);
|
||||
}
|
||||
|
||||
[CommandCmif(6)] // 1.0.0-6.2.0
|
||||
@@ -107,7 +107,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
|
||||
|
||||
// NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId.
|
||||
|
||||
return PrepareAddOnContentImpl(context, context.Device.Processes.ActiveApplication.ProgramId);
|
||||
return PrepareAddOnContentImpl(context, context.Device.Processes.GetProcess(pid).ProgramId);
|
||||
}
|
||||
|
||||
[CommandCmif(8)] // 4.0.0+
|
||||
@@ -138,7 +138,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
|
||||
// NOTE: Service call arp:r GetApplicationLaunchProperty to get TitleId using the PId.
|
||||
|
||||
// TODO: Found where stored value is used.
|
||||
ResultCode resultCode = GetAddOnContentBaseIdFromTitleId(context, context.Device.Processes.ActiveApplication.ProgramId);
|
||||
ResultCode resultCode = GetAddOnContentBaseIdFromTitleId(context, context.Device.Processes.GetProcess(pid).ProgramId);
|
||||
|
||||
if (resultCode != ResultCode.Success)
|
||||
{
|
||||
@@ -310,7 +310,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
|
||||
// NOTE: Service calls arp:r GetApplicationControlProperty to get AddOnContentBaseId using TitleId,
|
||||
// If the call fails, it returns ResultCode.InvalidPid.
|
||||
|
||||
_addOnContentBaseId = context.Device.Processes.ActiveApplication.ApplicationControlProperties.AddOnContentBaseId;
|
||||
_addOnContentBaseId = context.Device.Processes.GetProcess(context.ClientProcessId).ApplicationControlProperties.AddOnContentBaseId;
|
||||
|
||||
if (_addOnContentBaseId == 0)
|
||||
{
|
||||
@@ -324,7 +324,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns.Aoc
|
||||
{
|
||||
uint index = context.RequestData.ReadUInt32();
|
||||
|
||||
ResultCode resultCode = GetAddOnContentBaseIdFromTitleId(context, context.Device.Processes.ActiveApplication.ProgramId);
|
||||
ResultCode resultCode = GetAddOnContentBaseIdFromTitleId(context, titleId);
|
||||
|
||||
if (resultCode != ResultCode.Success)
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns
|
||||
|
||||
ulong position = context.Request.ReceiveBuff[0].Position;
|
||||
|
||||
ApplicationControlProperty nacp = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
|
||||
ApplicationControlProperty nacp = context.Device.Processes.GetProcess(context.ClientProcessId).ApplicationControlProperties;
|
||||
|
||||
context.Memory.Write(position, SpanHelpers.AsByteSpan(ref nacp).ToArray());
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Ryujinx.HLE.HOS.Services.Ns
|
||||
|
||||
ulong position = context.Request.ReceiveBuff[0].Position;
|
||||
|
||||
ApplicationControlProperty nacp = context.Device.Processes.ActiveApplication.ApplicationControlProperties;
|
||||
ApplicationControlProperty nacp = context.Device.Processes.GetProcess(context.ClientProcessId).ApplicationControlProperties;
|
||||
|
||||
context.Memory.Write(position, SpanHelpers.AsByteSpan(ref nacp).ToArray());
|
||||
|
||||
|
||||
@@ -48,21 +48,22 @@ namespace Ryujinx.HLE.HOS.Services.Pctl.ParentalControlServiceFactory
|
||||
{
|
||||
if ((_permissionFlag & 0x40) == 0)
|
||||
{
|
||||
ulong titleId = ApplicationLaunchProperty.GetByPid(context).TitleId;
|
||||
ulong titleId = ApplicationLaunchProperty.GetByPid(context, _pid).TitleId;
|
||||
|
||||
if (titleId != 0)
|
||||
{
|
||||
_titleId = titleId;
|
||||
var process = context.Device.Processes.GetProcess(_pid);
|
||||
|
||||
// TODO: Call nn::arp::GetApplicationControlProperty here when implemented, if it return ResultCode.Success we assign fields.
|
||||
_ratingAge = new int[context.Device.Processes.ActiveApplication.ApplicationControlProperties.RatingAge.Length];
|
||||
_ratingAge = new int[process.ApplicationControlProperties.RatingAge.Length];
|
||||
|
||||
for (int i = 0; i < _ratingAge.Length; i++)
|
||||
{
|
||||
_ratingAge[i] = Convert.ToInt32(context.Device.Processes.ActiveApplication.ApplicationControlProperties.RatingAge[i]);
|
||||
_ratingAge[i] = Convert.ToInt32(process.ApplicationControlProperties.RatingAge[i]);
|
||||
}
|
||||
|
||||
_parentalControlFlag = context.Device.Processes.ActiveApplication.ApplicationControlProperties.ParentalControlFlag;
|
||||
_parentalControlFlag = process.ApplicationControlProperties.ParentalControlFlag;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using Ryujinx.Common;
|
||||
using Ryujinx.Cpu;
|
||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
using Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService.Types;
|
||||
using Ryujinx.HLE.Loaders.Processes;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -31,7 +32,8 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService
|
||||
}
|
||||
}
|
||||
|
||||
PlayLogQueryCapability queryCapability = (PlayLogQueryCapability)context.Device.Processes.ActiveApplication.ApplicationControlProperties.PlayLogQueryCapability;
|
||||
ProcessResult process = context.Device.Processes.GetProcess(context.ClientProcessId);
|
||||
PlayLogQueryCapability queryCapability = (PlayLogQueryCapability)process.ApplicationControlProperties.PlayLogQueryCapability;
|
||||
|
||||
List<ulong> titleIds = [];
|
||||
|
||||
@@ -45,7 +47,7 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pdm.QueryService
|
||||
// Check if input title ids are in the whitelist.
|
||||
foreach (ulong titleId in titleIds)
|
||||
{
|
||||
if (!context.Device.Processes.ActiveApplication.ApplicationControlProperties.PlayLogQueryableApplicationId.AsReadOnlySpan().Contains(titleId))
|
||||
if (!process.ApplicationControlProperties.PlayLogQueryableApplicationId.AsReadOnlySpan().Contains(titleId))
|
||||
{
|
||||
return (ResultCode)Am.ResultCode.ObjectInvalid;
|
||||
}
|
||||
|
||||
34
src/Ryujinx.HLE/Loaders/Processes/ProcessIdentity.cs
Normal file
34
src/Ryujinx.HLE/Loaders/Processes/ProcessIdentity.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
namespace Ryujinx.HLE.Loaders.Processes
|
||||
{
|
||||
public readonly struct ProcessIdentity
|
||||
{
|
||||
public ulong ProcessId { get; }
|
||||
public ulong ProgramId { get; }
|
||||
public ulong ApplicationId { get; }
|
||||
public byte ProgramIndex { get; }
|
||||
public string ProgramIdText { get; }
|
||||
public string DisplayVersion { get; }
|
||||
public ProcessKind Kind { get; }
|
||||
|
||||
public ProcessIdentity(
|
||||
ulong processId,
|
||||
ulong programId,
|
||||
byte programIndex,
|
||||
string displayVersion,
|
||||
ProcessKind kind)
|
||||
{
|
||||
ProcessId = processId;
|
||||
ProgramId = programId;
|
||||
ProgramIndex = programIndex;
|
||||
ApplicationId = programId & ~0xFul;
|
||||
ProgramIdText = $"{programId:x16}";
|
||||
DisplayVersion = displayVersion ?? string.Empty;
|
||||
Kind = kind;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"{Kind} pid={ProcessId} program={ProgramIdText} application={ApplicationId:x16} index={ProgramIndex} version={DisplayVersion}";
|
||||
}
|
||||
}
|
||||
}
|
||||
12
src/Ryujinx.HLE/Loaders/Processes/ProcessKind.cs
Normal file
12
src/Ryujinx.HLE/Loaders/Processes/ProcessKind.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Ryujinx.HLE.Loaders.Processes
|
||||
{
|
||||
public enum ProcessKind
|
||||
{
|
||||
Unknown,
|
||||
Application,
|
||||
SystemApplication,
|
||||
SystemApplet,
|
||||
LibraryApplet,
|
||||
Homebrew,
|
||||
}
|
||||
}
|
||||
@@ -28,21 +28,61 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
|
||||
private ulong _latestPid;
|
||||
|
||||
private readonly object _pidLock = new();
|
||||
|
||||
#nullable enable
|
||||
public ProcessResult? ActiveApplication
|
||||
{
|
||||
get
|
||||
{
|
||||
return _processesByPid.GetValueOrDefault(_latestPid);
|
||||
lock (_pidLock)
|
||||
{
|
||||
// Check if _latestPid is still valid
|
||||
if (_latestPid == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Using this if statement locks up the UI and prevents a new game from loading.
|
||||
// Haven't quite deduced why yet.
|
||||
// Verify process still exists in kernel (authoritative source)
|
||||
if (!_device.System.KernelContext.Processes.TryGetValue(_latestPid, out HOS.Kernel.Process.KProcess? kernelProcess))
|
||||
{
|
||||
// Process no longer exists in kernel, clear stale state
|
||||
Logger.Warning?.Print(LogClass.Loader,
|
||||
$"ActiveApplication PID {_latestPid} no longer exists in kernel, clearing stale state");
|
||||
|
||||
if (!_processesByPid.TryGetValue(_latestPid, out ProcessResult value))
|
||||
throw new RyujinxException(
|
||||
$"The HLE Process map did not have a process with ID {_latestPid}. Are you missing firmware?");
|
||||
_processesByPid.TryRemove(_latestPid, out _);
|
||||
_latestPid = 0;
|
||||
TitleIDs.CurrentApplication.Value = null;
|
||||
|
||||
return value;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Verify process still exists in ProcessLoader's dictionary
|
||||
if (_processesByPid.TryGetValue(_latestPid, out ProcessResult? processResult))
|
||||
{
|
||||
// Additional check: verify process state
|
||||
if (kernelProcess.State == HOS.Kernel.Process.ProcessState.Exited ||
|
||||
kernelProcess.State == HOS.Kernel.Process.ProcessState.Exiting)
|
||||
{
|
||||
Logger.Warning?.Print(LogClass.Loader,
|
||||
$"ActiveApplication PID {_latestPid} is in state {kernelProcess.State}, clearing");
|
||||
|
||||
_processesByPid.TryRemove(_latestPid, out _);
|
||||
_latestPid = 0;
|
||||
TitleIDs.CurrentApplication.Value = null;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return processResult;
|
||||
}
|
||||
|
||||
// Fallback: clear stale PID if not in our dictionary
|
||||
Logger.Warning?.Print(LogClass.Loader,
|
||||
$"ActiveApplication PID {_latestPid} not in ProcessLoader dictionary, clearing");
|
||||
_latestPid = 0;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
#nullable disable
|
||||
@@ -53,6 +93,23 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
_processesByPid = new ConcurrentDictionary<ulong, ProcessResult>();
|
||||
}
|
||||
|
||||
public bool TryGetProcess(ulong pid, out ProcessResult process)
|
||||
{
|
||||
return _processesByPid.TryGetValue(pid, out process);
|
||||
}
|
||||
|
||||
public ProcessResult GetProcess(ulong pid)
|
||||
{
|
||||
if (_processesByPid.TryGetValue(pid, out ProcessResult process))
|
||||
{
|
||||
return process;
|
||||
}
|
||||
|
||||
Logger.Warning?.Print(LogClass.Loader, $"Process metadata for pid {pid} was not found. Falling back to active application metadata.");
|
||||
|
||||
return ActiveApplication;
|
||||
}
|
||||
|
||||
public bool LoadXci(string path, ulong applicationId)
|
||||
{
|
||||
FileStream stream = new(path, FileMode.Open, FileAccess.Read);
|
||||
@@ -285,5 +342,39 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears a specific process from the ProcessLoader's tracking.
|
||||
/// This should be called when a process exits or is terminated.
|
||||
/// </summary>
|
||||
/// <param name="pid">The process ID to clear</param>
|
||||
public void ClearProcess(ulong pid)
|
||||
{
|
||||
lock (_pidLock)
|
||||
{
|
||||
if (_processesByPid.TryRemove(pid, out _))
|
||||
{
|
||||
if (_latestPid == pid)
|
||||
{
|
||||
_latestPid = 0;
|
||||
TitleIDs.CurrentApplication.Value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all processes from the ProcessLoader's tracking.
|
||||
/// This should be called during system shutdown.
|
||||
/// </summary>
|
||||
public void ClearAllProcesses()
|
||||
{
|
||||
lock (_pidLock)
|
||||
{
|
||||
_processesByPid.Clear();
|
||||
_latestPid = 0;
|
||||
TitleIDs.CurrentApplication.Value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -436,6 +436,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
allowCodeMemoryForJit,
|
||||
processContextFactory.DiskCacheLoadState,
|
||||
process.Pid,
|
||||
programIndex,
|
||||
meta.MainThreadPriority,
|
||||
meta.MainThreadStackSize,
|
||||
device.System.State.DesiredTitleLanguage);
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
{
|
||||
public class ProcessResult
|
||||
{
|
||||
public static ProcessResult Failed => new(null, new BlitStruct<ApplicationControlProperty>(1), false, false, null, 0, 0, 0, TitleLanguage.AmericanEnglish);
|
||||
public static ProcessResult Failed => new(null, new BlitStruct<ApplicationControlProperty>(1), false, false, null, 0, 0, 0, 0, TitleLanguage.AmericanEnglish);
|
||||
|
||||
private readonly byte _mainThreadPriority;
|
||||
private readonly uint _mainThreadStackSize;
|
||||
@@ -28,6 +28,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
public readonly bool Is64Bit;
|
||||
public readonly bool DiskCacheEnabled;
|
||||
public readonly bool AllowCodeMemoryForJit;
|
||||
public readonly ProcessIdentity Identity;
|
||||
|
||||
public ProcessResult(
|
||||
MetaLoader metaLoader,
|
||||
@@ -36,6 +37,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
bool allowCodeMemoryForJit,
|
||||
IDiskCacheLoadState diskCacheLoadState,
|
||||
ulong pid,
|
||||
byte programIndex,
|
||||
byte mainThreadPriority,
|
||||
uint mainThreadStackSize,
|
||||
TitleLanguage titleLanguage)
|
||||
@@ -71,6 +73,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
ProgramId = programId;
|
||||
ProgramIdText = $"{programId:x16}";
|
||||
Is64Bit = metaLoader.IsProgram64Bit;
|
||||
Identity = new ProcessIdentity(pid, programId, programIndex, DisplayVersion, GetProcessKind(programId));
|
||||
}
|
||||
|
||||
else
|
||||
@@ -84,6 +87,31 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
AllowCodeMemoryForJit = allowCodeMemoryForJit;
|
||||
}
|
||||
|
||||
private static ProcessKind GetProcessKind(ulong programId)
|
||||
{
|
||||
if (programId == 0)
|
||||
{
|
||||
return ProcessKind.Application;
|
||||
}
|
||||
|
||||
if (programId is >= 0x0100000000001000 and <= 0x0100000000001FFF)
|
||||
{
|
||||
return ProcessKind.SystemApplet;
|
||||
}
|
||||
|
||||
if (programId is >= 0x0100000000000800 and <= 0x0100000000000FFF)
|
||||
{
|
||||
return ProcessKind.LibraryApplet;
|
||||
}
|
||||
|
||||
if (programId <= 0x0100000000007FFF)
|
||||
{
|
||||
return ProcessKind.SystemApplication;
|
||||
}
|
||||
|
||||
return ProcessKind.Application;
|
||||
}
|
||||
|
||||
public bool Start(Switch device)
|
||||
{
|
||||
device.Configuration.ContentManager.LoadEntries(device);
|
||||
@@ -109,6 +137,7 @@ namespace Ryujinx.HLE.Loaders.Processes
|
||||
: device.System.ContentManager.GetCurrentFirmwareVersion()?.VersionString ?? "?";
|
||||
|
||||
Logger.Info?.Print(LogClass.Loader, $"Application Loaded: {name} v{version} [{ProgramIdText}] [{(Is64Bit ? "64-bit" : "32-bit")}]");
|
||||
Logger.Info?.Print(LogClass.Loader, $"Process identity: {Identity}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -183,6 +183,7 @@ namespace Ryujinx.HLE
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
Processes.ClearAllProcesses();
|
||||
System.Dispose();
|
||||
AudioDeviceDriver.Dispose();
|
||||
FileSystem.Dispose();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.HOS.Services.Hid;
|
||||
using SDL;
|
||||
using static SDL.SDL3;
|
||||
@@ -13,6 +14,7 @@ namespace Ryujinx.Input.SDL3
|
||||
private readonly SDL_hid_device* _hidHandle;
|
||||
|
||||
private int _globalCount;
|
||||
private ulong _lastWriteTicks;
|
||||
|
||||
private NpadHdRumble(SDL_hid_device* hidHandle)
|
||||
{
|
||||
@@ -28,7 +30,7 @@ namespace Ryujinx.Input.SDL3
|
||||
}
|
||||
|
||||
ushort product = SDL_GetGamepadProduct(gamepadHandle);
|
||||
if (product != 0x2006 && product != 0x2007 && product != 0x2009 && product != 0x200e)
|
||||
if (!Enum.IsDefined(typeof(HDRumbleSupported), product))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -37,7 +39,7 @@ namespace Ryujinx.Input.SDL3
|
||||
}
|
||||
|
||||
// Some of the code was translated from https://github.com/MIZUSHIKI/JoyShockLibrary-plus-HDRumble
|
||||
private void WriteHdRumble(
|
||||
private bool WriteHdRumble(
|
||||
int encLeftLowFreq, int encLeftLowAmp,
|
||||
int encLeftHighFreq, int encLeftHighAmp,
|
||||
int encRightLowFreq, int encRightLowAmp,
|
||||
@@ -65,26 +67,35 @@ namespace Ryujinx.Input.SDL3
|
||||
|
||||
fixed (byte* ptr = buf)
|
||||
{
|
||||
SDL_hid_write(_hidHandle, ptr, (nuint)buf.Length);
|
||||
if (SendHDRumble(ptr, (nuint)buf.Length) >= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!String.IsNullOrEmpty(SDL_GetError()))
|
||||
{
|
||||
Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError());
|
||||
SDL_ClearError();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static int EncodeLowFreq(float lowFreq)
|
||||
{
|
||||
float lf = Math.Clamp(lowFreq, 40.875885f, 626.286133f);
|
||||
return (int)Math.Round(32 * Math.Log2(lf * 0.1f)) - 0x40;
|
||||
return (int) Math.Round(32 * Math.Log2(lf * 0.1f) - 0x40);
|
||||
}
|
||||
|
||||
private static int EncodeHighFreq(float highFreq)
|
||||
{
|
||||
float hf = Math.Clamp(highFreq, 81.75177f, 1252.572266f);
|
||||
return ((int)Math.Round(32 * Math.Log2(hf * 0.1f)) - 0x60) * 4;
|
||||
return (int) Math.Round((32 * Math.Log2(hf * 0.1f) - 0x60) * 4);
|
||||
}
|
||||
|
||||
private static int EncodeLowAmp(float rawAmp)
|
||||
{
|
||||
int encodedAmp = 0;
|
||||
double encodedAmp = 0;
|
||||
|
||||
if (rawAmp is > 0 and < 0.012f)
|
||||
{
|
||||
@@ -92,15 +103,15 @@ namespace Ryujinx.Input.SDL3
|
||||
}
|
||||
else if (rawAmp is >= 0.012f and < 0.112f)
|
||||
{
|
||||
encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f));
|
||||
encodedAmp = 4 * Math.Log2(rawAmp * 110f);
|
||||
}
|
||||
else if (rawAmp is >= 0.112f and < 0.225f)
|
||||
{
|
||||
encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f));
|
||||
encodedAmp = 16 * Math.Log2(rawAmp * 17f);
|
||||
}
|
||||
else if (rawAmp is >= 0.225f and <= 1f)
|
||||
{
|
||||
encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f));
|
||||
encodedAmp = 32 * Math.Log2(rawAmp * 8.7f);
|
||||
}
|
||||
|
||||
return (int)Math.Floor(encodedAmp / 2.0) + 64;
|
||||
@@ -108,7 +119,7 @@ namespace Ryujinx.Input.SDL3
|
||||
|
||||
private static int EncodeHighAmp(float rawAmp)
|
||||
{
|
||||
int encodedAmp = 0;
|
||||
double encodedAmp = 0;
|
||||
|
||||
if (rawAmp is > 0 and < 0.012f)
|
||||
{
|
||||
@@ -116,23 +127,23 @@ namespace Ryujinx.Input.SDL3
|
||||
}
|
||||
else if (rawAmp is >= 0.012f and < 0.112f)
|
||||
{
|
||||
encodedAmp = (int)Math.Round(4 * Math.Log2(rawAmp * 110f));
|
||||
encodedAmp = 4 * Math.Log2(rawAmp * 110f);
|
||||
}
|
||||
else if (rawAmp is >= 0.112f and < 0.225f)
|
||||
{
|
||||
encodedAmp = (int)Math.Round(16 * Math.Log2(rawAmp * 17f));
|
||||
encodedAmp = 16 * Math.Log2(rawAmp * 17f);
|
||||
}
|
||||
else if (rawAmp is >= 0.225f and <= 1f)
|
||||
{
|
||||
encodedAmp = (int)Math.Round(32 * Math.Log2(rawAmp * 8.7f));
|
||||
encodedAmp = 32 * Math.Log2(rawAmp * 8.7f);
|
||||
}
|
||||
|
||||
return encodedAmp * 2;
|
||||
return (int) Math.Round(encodedAmp * 2);
|
||||
}
|
||||
|
||||
public bool HdRumble(VibrationValue left, VibrationValue right)
|
||||
{
|
||||
WriteHdRumble(EncodeLowFreq(left.FrequencyLow),
|
||||
return WriteHdRumble(EncodeLowFreq(left.FrequencyLow),
|
||||
EncodeLowAmp(left.AmplitudeLow),
|
||||
EncodeHighFreq(left.FrequencyHigh),
|
||||
EncodeHighAmp(left.AmplitudeHigh),
|
||||
@@ -140,7 +151,34 @@ namespace Ryujinx.Input.SDL3
|
||||
EncodeLowAmp(right.AmplitudeLow),
|
||||
EncodeHighFreq(right.FrequencyHigh),
|
||||
EncodeHighAmp(right.AmplitudeHigh));
|
||||
return true;
|
||||
}
|
||||
|
||||
private int SendHDRumble(byte* data, nuint length)
|
||||
{
|
||||
int result = 0;
|
||||
ulong currentTicks = SDL_GetTicks();
|
||||
|
||||
// Ditch rumble if we haven't hit the poll-rate yet.
|
||||
// TODO: figure out a better way to do this
|
||||
// While the polling check makes the rumble accurate, it also causes it to miss signals.
|
||||
if ((currentTicks - _lastWriteTicks) < 8) // https://docs.handheldlegend.com/s/progcc-3/doc/lag-comparison-aAR1mV3JLX
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
SDL_LockJoysticks();
|
||||
{
|
||||
// Fun fact: Mario Kart 8 Deluxe sends rumble packets
|
||||
// where the amplitude is zero, but the frequency isn't.
|
||||
result = SDL_hid_write(_hidHandle, data, length);
|
||||
if (result >= 0)
|
||||
{
|
||||
_lastWriteTicks = currentTicks;
|
||||
}
|
||||
}
|
||||
SDL_UnlockJoysticks();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@@ -148,4 +186,18 @@ namespace Ryujinx.Input.SDL3
|
||||
SDL_hid_close(_hidHandle);
|
||||
}
|
||||
}
|
||||
|
||||
public enum HDRumbleSupported : ushort
|
||||
{
|
||||
JoyConLeft = 0x2006,
|
||||
JoyConRight = 0x2007,
|
||||
JoyconPair = 0x2008,
|
||||
ProController = 0x2009,
|
||||
JoyconGrip = 0x200e,
|
||||
Joycon2Right = 0x2066,
|
||||
Joycon2Left = 0x2067,
|
||||
Joycon2Pair = 0x2068,
|
||||
Switch2ProController = 0x2069,
|
||||
GamecubeController = 0x2073
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,6 +165,9 @@ namespace Ryujinx.Input.SDL3
|
||||
public string Id { get; }
|
||||
public string Name { get; }
|
||||
|
||||
// Expose vendor id for higher-fidelity device detection in UI
|
||||
public ushort VendorId => _gamepadHandle != null ? SDL_GetGamepadVendor(_gamepadHandle) : (ushort)0;
|
||||
|
||||
public bool IsConnected => SDL_GamepadConnected(_gamepadHandle);
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
@@ -197,10 +200,12 @@ namespace Ryujinx.Input.SDL3
|
||||
return _hdRumble?.HdRumble(left, right) ?? false;
|
||||
}
|
||||
|
||||
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
||||
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
||||
{
|
||||
if ((Features & GamepadFeaturesFlag.Rumble) == 0)
|
||||
return;
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
|
||||
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
|
||||
@@ -219,6 +224,15 @@ namespace Ryujinx.Input.SDL3
|
||||
if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs))
|
||||
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller.");
|
||||
}
|
||||
|
||||
if (!String.IsNullOrEmpty(SDL_GetError()))
|
||||
{
|
||||
Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError());
|
||||
SDL_ClearError();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public Vector3 GetMotionData(MotionInputId inputId)
|
||||
|
||||
@@ -162,7 +162,7 @@ namespace Ryujinx.Input.SDL3
|
||||
|
||||
public void SetTriggerThreshold(float triggerThreshold)
|
||||
{
|
||||
|
||||
// No operations
|
||||
}
|
||||
|
||||
public bool HDRumble(VibrationValue left, VibrationValue right)
|
||||
@@ -170,10 +170,12 @@ namespace Ryujinx.Input.SDL3
|
||||
return _hdRumble?.HdRumble(left, right) ?? false;
|
||||
}
|
||||
|
||||
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
||||
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
||||
{
|
||||
if ((Features & GamepadFeaturesFlag.Rumble) == 0)
|
||||
return;
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ushort lowFrequencyRaw = (ushort)(lowFrequency * ushort.MaxValue);
|
||||
ushort highFrequencyRaw = (ushort)(highFrequency * ushort.MaxValue);
|
||||
@@ -192,6 +194,15 @@ namespace Ryujinx.Input.SDL3
|
||||
if (!SDL_RumbleGamepad(_gamepadHandle, lowFrequencyRaw, highFrequencyRaw, durationMs))
|
||||
Logger.Error?.Print(LogClass.Hid, "Rumble is not supported on this game controller.");
|
||||
}
|
||||
|
||||
if (!String.IsNullOrEmpty(SDL_GetError()))
|
||||
{
|
||||
Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError());
|
||||
SDL_ClearError();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public Vector3 GetMotionData(MotionInputId inputId)
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using Gommon;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.HOS.Services.Hid;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
@@ -61,7 +64,14 @@ namespace Ryujinx.Input.SDL3
|
||||
return left.IsPressed(inputId) || right.IsPressed(inputId);
|
||||
}
|
||||
|
||||
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
||||
public bool HDRumble(VibrationValue left, VibrationValue right)
|
||||
{
|
||||
// return _hdRumble?.HdRumble(left, right) ?? false;
|
||||
// TODO: Track rumble and motion on both controllers
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
||||
{
|
||||
if (lowFrequency != 0)
|
||||
{
|
||||
@@ -78,6 +88,15 @@ namespace Ryujinx.Input.SDL3
|
||||
left.Rumble(0, 0, durationMs);
|
||||
right.Rumble(0, 0, durationMs);
|
||||
}
|
||||
|
||||
if (!SDL_GetError().IsNullOrEmpty())
|
||||
{
|
||||
Logger.Error?.PrintMsg(LogClass.Hid, SDL_GetError());
|
||||
SDL_ClearError();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetConfiguration(InputConfig configuration)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.HOS.Services.Hid;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
@@ -8,25 +9,15 @@ using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using SDL;
|
||||
using static SDL.SDL3;
|
||||
|
||||
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
|
||||
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||
|
||||
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 readonly List<KeyboardInputMappingHelper.KeyboardButtonMapping> _buttonsUserMapping;
|
||||
|
||||
|
||||
private static readonly SDL_Keycode[] _keysDriverMapping =
|
||||
@@ -171,9 +162,8 @@ namespace Ryujinx.Input.SDL3
|
||||
SDL_Keycode.SDLK_0
|
||||
];
|
||||
|
||||
public SDL3Keyboard(SDL3KeyboardDriver driver, string id, string name)
|
||||
public SDL3Keyboard(string id, string name)
|
||||
{
|
||||
_driver = driver;
|
||||
Id = id;
|
||||
Name = name;
|
||||
_buttonsUserMapping = [];
|
||||
@@ -195,9 +185,9 @@ namespace Ryujinx.Input.SDL3
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private unsafe static int ToSDL3Scancode(Key key)
|
||||
private unsafe static int ToSDL3Scancode(ConfigPhysicalKey key)
|
||||
{
|
||||
if (key is >= Key.Unknown and <= Key.Menu)
|
||||
if (key is >= ConfigPhysicalKey.Unknown and <= ConfigPhysicalKey.Menu)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
@@ -205,18 +195,18 @@ namespace Ryujinx.Input.SDL3
|
||||
return (int)SDL_GetScancodeFromKey(_keysDriverMapping[(int)key], null);
|
||||
}
|
||||
|
||||
private static SDL_Keymod GetKeyboardModifierMask(Key key)
|
||||
private static SDL_Keymod GetKeyboardModifierMask(ConfigPhysicalKey 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,
|
||||
ConfigPhysicalKey.ShiftLeft => SDL_Keymod.SDL_KMOD_LSHIFT,
|
||||
ConfigPhysicalKey.ShiftRight => SDL_Keymod.SDL_KMOD_RSHIFT,
|
||||
ConfigPhysicalKey.ControlLeft => SDL_Keymod.SDL_KMOD_LCTRL,
|
||||
ConfigPhysicalKey.ControlRight => SDL_Keymod.SDL_KMOD_RCTRL,
|
||||
ConfigPhysicalKey.AltLeft => SDL_Keymod.SDL_KMOD_LALT,
|
||||
ConfigPhysicalKey.AltRight => SDL_Keymod.SDL_KMOD_RALT,
|
||||
ConfigPhysicalKey.WinLeft => SDL_Keymod.SDL_KMOD_LGUI,
|
||||
ConfigPhysicalKey.WinRight => SDL_Keymod.SDL_KMOD_RGUI,
|
||||
// NOTE: Menu key isn't supported by SDL3.
|
||||
_ => SDL_Keymod.SDL_KMOD_NONE
|
||||
};
|
||||
@@ -232,9 +222,9 @@ namespace Ryujinx.Input.SDL3
|
||||
rawKeyboardState = SDL_GetKeyboardState(null);
|
||||
}
|
||||
|
||||
bool[] keysState = new bool[(int)Key.Count];
|
||||
bool[] keysState = new bool[(int)ConfigPhysicalKey.Count];
|
||||
|
||||
for (Key key = 0; key < Key.Count; key++)
|
||||
for (ConfigPhysicalKey key = 0; key < ConfigPhysicalKey.Count; key++)
|
||||
{
|
||||
int index = ToSDL3Scancode(key);
|
||||
if (index == -1)
|
||||
@@ -264,36 +254,6 @@ namespace Ryujinx.Input.SDL3
|
||||
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();
|
||||
@@ -306,9 +266,9 @@ namespace Ryujinx.Input.SDL3
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
|
||||
foreach (KeyboardInputMappingHelper.KeyboardButtonMapping entry in _buttonsUserMapping)
|
||||
{
|
||||
if (entry.From == Key.Unknown || entry.From == Key.Unbound || entry.To == GamepadButtonInputId.Unbound)
|
||||
if (!entry.IsValid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -320,8 +280,8 @@ namespace Ryujinx.Input.SDL3
|
||||
}
|
||||
}
|
||||
|
||||
(short leftStickX, short leftStickY) = GetStickValues(ref rawState, _configuration.LeftJoyconStick);
|
||||
(short rightStickX, short rightStickY) = GetStickValues(ref rawState, _configuration.RightJoyconStick);
|
||||
(short leftStickX, short leftStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.LeftJoyconStick);
|
||||
(short rightStickX, short rightStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.RightJoyconStick);
|
||||
|
||||
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
|
||||
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
|
||||
@@ -357,38 +317,15 @@ namespace Ryujinx.Input.SDL3
|
||||
{
|
||||
_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));
|
||||
_buttonsUserMapping.AddRange(KeyboardInputMappingHelper.BuildButtonMappings(_configuration));
|
||||
}
|
||||
}
|
||||
|
||||
public void SetLed(uint packedRgb)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.UI, "SetLed called on an SDL3Keyboard");
|
||||
Logger.Debug?.Print(LogClass.UI, "SetLed called on an SDL3Keyboard");
|
||||
}
|
||||
|
||||
public void SetTriggerThreshold(float triggerThreshold)
|
||||
@@ -396,9 +333,14 @@ namespace Ryujinx.Input.SDL3
|
||||
// No operations
|
||||
}
|
||||
|
||||
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
||||
public bool HDRumble(VibrationValue left, VibrationValue right)
|
||||
{
|
||||
// No operations
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public Vector3 GetMotionData(MotionInputId inputId)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.HOS.Services.Hid;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Numerics;
|
||||
@@ -67,7 +68,12 @@ namespace Ryujinx.Input.SDL3
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
||||
public bool HDRumble(VibrationValue left, VibrationValue right)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Ryujinx.Input.SDL3
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SDL3Keyboard(this, _keyboardIdentifers[0], "All keyboards");
|
||||
return new SDL3Keyboard(_keyboardIdentifers[0], "All keyboards");
|
||||
}
|
||||
|
||||
public IEnumerable<IGamepad> GetGamepads()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using Ryujinx.Common.Logging;
|
||||
|
||||
namespace Ryujinx.Input.Assigner
|
||||
{
|
||||
/// <summary>
|
||||
@@ -8,22 +10,40 @@ namespace Ryujinx.Input.Assigner
|
||||
private readonly IKeyboard _keyboard;
|
||||
|
||||
private KeyboardStateSnapshot _keyboardState;
|
||||
private Button? _pressedButton;
|
||||
|
||||
public KeyboardKeyAssigner(IKeyboard keyboard)
|
||||
{
|
||||
_keyboard = keyboard;
|
||||
}
|
||||
|
||||
public void Initialize() { }
|
||||
public void Initialize()
|
||||
{
|
||||
_pressedButton = null;
|
||||
}
|
||||
|
||||
public void ReadInput()
|
||||
{
|
||||
_keyboardState = _keyboard.GetKeyboardStateSnapshot();
|
||||
|
||||
if (_pressedButton is null)
|
||||
{
|
||||
Button? buttonFromState = GetPressedButtonFromState();
|
||||
Button? buttonFromBufferedPress = buttonFromState is null ? GetPressedButtonFromBufferedPress() : null;
|
||||
|
||||
_pressedButton = buttonFromState ?? buttonFromBufferedPress;
|
||||
}
|
||||
|
||||
if (_pressedButton is not null)
|
||||
{
|
||||
string source = _pressedButton.HasValue && GetPressedButtonFromState() is not null ? "state" : "buffered-press";
|
||||
Logger.Debug?.Print(LogClass.UI, $"Keyboard assigner registered key={_pressedButton.Value.AsHidType<Key>()}, source={source}, cancelPressed={ShouldCancel()}");
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAnyButtonPressed()
|
||||
{
|
||||
return GetPressedButton() is not null;
|
||||
return _pressedButton is not null;
|
||||
}
|
||||
|
||||
public bool ShouldCancel()
|
||||
@@ -33,18 +53,53 @@ namespace Ryujinx.Input.Assigner
|
||||
|
||||
public Button? GetPressedButton()
|
||||
{
|
||||
Button? keyPressed = null;
|
||||
return !ShouldCancel() ? _pressedButton : null;
|
||||
}
|
||||
|
||||
private Button? GetPressedButtonFromState()
|
||||
{
|
||||
Key aliasedKey = GetAliasedPressedKey();
|
||||
|
||||
if (aliasedKey != Key.Unknown)
|
||||
{
|
||||
return new Button(aliasedKey);
|
||||
}
|
||||
|
||||
for (Key key = Key.Unknown; key < Key.Count; key++)
|
||||
{
|
||||
if (_keyboardState.IsPressed(key))
|
||||
{
|
||||
keyPressed = new(key);
|
||||
break;
|
||||
return new Button(key);
|
||||
}
|
||||
}
|
||||
|
||||
return !ShouldCancel() ? keyPressed : null;
|
||||
return null;
|
||||
}
|
||||
|
||||
private Button? GetPressedButtonFromBufferedPress()
|
||||
{
|
||||
return _keyboard.TryConsumePressedKey(out Key key) ? new Button(key) : null;
|
||||
}
|
||||
|
||||
private Key GetAliasedPressedKey()
|
||||
{
|
||||
// On some layouts (for example AltGr on Windows), Right Alt is reported as Ctrl+Alt.
|
||||
// Prefer AltRight in that case so the binding reflects the physical key used.
|
||||
if (_keyboardState.IsPressed(Key.ControlLeft) && _keyboardState.IsPressed(Key.AltRight))
|
||||
{
|
||||
return Key.AltRight;
|
||||
}
|
||||
|
||||
// On some Copilot keyboards, the key in the right-control position is reported as
|
||||
// ShiftLeft+Win+F23. Prefer ControlRight so the binding reflects that physical key.
|
||||
if (_keyboardState.IsPressed(Key.ShiftLeft) &&
|
||||
_keyboardState.IsPressed(Key.F23) &&
|
||||
(_keyboardState.IsPressed(Key.WinLeft) || _keyboardState.IsPressed(Key.WinRight)))
|
||||
{
|
||||
return Key.ControlRight;
|
||||
}
|
||||
|
||||
return Key.Unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ using Ryujinx.Common;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.HOS.Services.Hid;
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
@@ -234,7 +234,9 @@ namespace Ryujinx.Input.HLE
|
||||
_gamepad?.Dispose();
|
||||
|
||||
Id = config.Id;
|
||||
_gamepad = GamepadDriver.GetGamepad(Id);
|
||||
_gamepad = config is StandardKeyboardInputConfig && GamepadDriver is IKeyboardModeDriver keyboardModeDriver
|
||||
? keyboardModeDriver.GetKeyboard(Id, KeyboardInputMode.Physical)
|
||||
: GamepadDriver.GetGamepad(Id);
|
||||
|
||||
UpdateUserConfiguration(config);
|
||||
|
||||
@@ -555,34 +557,37 @@ namespace Ryujinx.Input.HLE
|
||||
{
|
||||
if (queue.TryDequeue(out (VibrationValue, VibrationValue) dualVibrationValue))
|
||||
{
|
||||
if (_config is StandardControllerInputConfig controllerConfig && controllerConfig.Rumble.EnableRumble)
|
||||
if (_config is not StandardControllerInputConfig controllerConfig ||
|
||||
!controllerConfig.Rumble.EnableRumble)
|
||||
{
|
||||
VibrationValue leftVibrationValue = dualVibrationValue.Item1;
|
||||
VibrationValue rightVibrationValue = dualVibrationValue.Item2;
|
||||
|
||||
float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15) * controllerConfig.Rumble.StrongRumble));
|
||||
float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85) * controllerConfig.Rumble.WeakRumble));
|
||||
|
||||
leftVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
|
||||
leftVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
|
||||
rightVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
|
||||
rightVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
|
||||
|
||||
if (_gamepad?.HDRumble(leftVibrationValue, rightVibrationValue) == false)
|
||||
{
|
||||
_gamepad?.Rumble(low, high, uint.MaxValue);
|
||||
}
|
||||
|
||||
Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " +
|
||||
$"L.low.amp={leftVibrationValue.AmplitudeLow}, " +
|
||||
$"L.high.amp={leftVibrationValue.AmplitudeHigh}, " +
|
||||
$"L.low.freq={leftVibrationValue.FrequencyLow}, " +
|
||||
$"L.high.freq={leftVibrationValue.FrequencyHigh}, " +
|
||||
$"R.low.amp={rightVibrationValue.AmplitudeLow}, " +
|
||||
$"R.high.amp={rightVibrationValue.AmplitudeHigh} " +
|
||||
$"R.low.freq={rightVibrationValue.FrequencyLow}, " +
|
||||
$"R.high.freq={rightVibrationValue.FrequencyHigh}");
|
||||
return;
|
||||
}
|
||||
|
||||
VibrationValue leftVibrationValue = dualVibrationValue.Item1;
|
||||
VibrationValue rightVibrationValue = dualVibrationValue.Item2;
|
||||
|
||||
leftVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
|
||||
leftVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
|
||||
rightVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
|
||||
rightVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
|
||||
|
||||
if (!controllerConfig.Rumble.UseHDRumble || _gamepad?.HDRumble(leftVibrationValue, rightVibrationValue) == false)
|
||||
{
|
||||
float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15)));
|
||||
float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85)));
|
||||
_gamepad?.Rumble(low, high, 0xFFFFFFFF);
|
||||
}
|
||||
|
||||
Logger.Debug?.Print(LogClass.Hid, $"Effect for {controllerConfig.PlayerIndex} " +
|
||||
// Value=value/multiplier * multiplier (result)
|
||||
$"L.low.amp={leftVibrationValue.AmplitudeLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.AmplitudeLow}), " +
|
||||
$"L.high.amp={leftVibrationValue.AmplitudeHigh / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.AmplitudeHigh}), " +
|
||||
$"L.low.freq={leftVibrationValue.FrequencyLow / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.FrequencyLow}), " +
|
||||
$"L.high.freq={leftVibrationValue.FrequencyHigh / controllerConfig.Rumble.WeakRumble} * {controllerConfig.Rumble.WeakRumble} ({leftVibrationValue.FrequencyHigh}), " +
|
||||
$"R.low.amp={rightVibrationValue.AmplitudeLow / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.AmplitudeLow}), " +
|
||||
$"R.high.amp={rightVibrationValue.AmplitudeHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.AmplitudeHigh}), " +
|
||||
$"R.low.freq={rightVibrationValue.FrequencyLow / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.FrequencyLow}), " +
|
||||
$"R.high.freq={rightVibrationValue.FrequencyHigh / controllerConfig.Rumble.StrongRumble} * {controllerConfig.Rumble.StrongRumble} ({rightVibrationValue.FrequencyHigh})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace Ryujinx.Input.HLE
|
||||
|
||||
private readonly Lock _lock = new();
|
||||
|
||||
private bool _blockInputUpdates;
|
||||
private int _inputUpdateBlockCount;
|
||||
|
||||
private const int MaxControllers = 9;
|
||||
|
||||
@@ -36,6 +36,7 @@ namespace Ryujinx.Input.HLE
|
||||
private bool _isDisposed;
|
||||
|
||||
private List<InputConfig> _inputConfig;
|
||||
private List<InputConfig> _requestedInputConfig;
|
||||
private bool _enableKeyboard;
|
||||
private bool _enableMouse;
|
||||
private Switch _device;
|
||||
@@ -52,6 +53,7 @@ namespace Ryujinx.Input.HLE
|
||||
_gamepadDriver = gamepadDriver;
|
||||
_mouseDriver = mouseDriver;
|
||||
_inputConfig = [];
|
||||
_requestedInputConfig = [];
|
||||
|
||||
_gamepadDriver.OnGamepadConnected += HandleOnGamepadConnected;
|
||||
_gamepadDriver.OnGamepadDisconnected += HandleOnGamepadDisconnected;
|
||||
@@ -89,29 +91,23 @@ namespace Ryujinx.Input.HLE
|
||||
}
|
||||
}
|
||||
|
||||
ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse);
|
||||
ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleOnGamepadConnected(string id)
|
||||
private void HandleOnGamepadConnected(string _)
|
||||
{
|
||||
// Force input reload
|
||||
ReloadConfiguration(_inputConfig, _enableKeyboard, _enableMouse);
|
||||
ReloadConfiguration(_requestedInputConfig, _enableKeyboard, _enableMouse);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private bool DriverConfigurationUpdate(ref NpadController controller, InputConfig config)
|
||||
{
|
||||
IGamepadDriver targetDriver = _gamepadDriver;
|
||||
|
||||
if (config is StandardControllerInputConfig)
|
||||
{
|
||||
targetDriver = _gamepadDriver;
|
||||
}
|
||||
else if (config is StandardKeyboardInputConfig)
|
||||
{
|
||||
targetDriver = _keyboardDriver;
|
||||
}
|
||||
IGamepadDriver targetDriver =
|
||||
config is StandardKeyboardInputConfig
|
||||
? _keyboardDriver
|
||||
: _gamepadDriver;
|
||||
|
||||
Debug.Assert(targetDriver != null, "Unknown input configuration!");
|
||||
|
||||
@@ -127,11 +123,13 @@ namespace Ryujinx.Input.HLE
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_requestedInputConfig = inputConfig?.ToList() ?? [];
|
||||
|
||||
NpadController[] oldControllers = _controllers.ToArray();
|
||||
|
||||
List<InputConfig> validInputs = [];
|
||||
|
||||
foreach (InputConfig inputConfigEntry in inputConfig)
|
||||
foreach (InputConfig inputConfigEntry in _requestedInputConfig)
|
||||
{
|
||||
NpadController controller;
|
||||
int index = (int)inputConfigEntry.PlayerIndex;
|
||||
@@ -147,7 +145,16 @@ namespace Ryujinx.Input.HLE
|
||||
controller = new(_cemuHookClient);
|
||||
}
|
||||
|
||||
bool isValid = DriverConfigurationUpdate(ref controller, inputConfigEntry);
|
||||
InputConfig activeConfig = inputConfigEntry;
|
||||
bool isValid = DriverConfigurationUpdate(ref controller, activeConfig);
|
||||
|
||||
if (!isValid &&
|
||||
inputConfigEntry is StandardControllerInputConfig &&
|
||||
TryGetKeyboardFallback(inputConfigEntry, out StandardKeyboardInputConfig fallbackConfig))
|
||||
{
|
||||
activeConfig = fallbackConfig;
|
||||
isValid = DriverConfigurationUpdate(ref controller, activeConfig);
|
||||
}
|
||||
|
||||
if (!isValid)
|
||||
{
|
||||
@@ -157,7 +164,7 @@ namespace Ryujinx.Input.HLE
|
||||
else
|
||||
{
|
||||
_controllers[index] = controller;
|
||||
validInputs.Add(inputConfigEntry);
|
||||
validInputs.Add(activeConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,7 +176,7 @@ namespace Ryujinx.Input.HLE
|
||||
oldControllers[i] = null;
|
||||
}
|
||||
|
||||
_inputConfig = inputConfig;
|
||||
_inputConfig = validInputs;
|
||||
_enableKeyboard = enableKeyboard;
|
||||
_enableMouse = enableMouse;
|
||||
|
||||
@@ -177,16 +184,58 @@ namespace Ryujinx.Input.HLE
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetKeyboardFallback(InputConfig inputConfig, out StandardKeyboardInputConfig fallbackConfig)
|
||||
{
|
||||
fallbackConfig = null;
|
||||
|
||||
ReadOnlySpan<string> keyboardIds = _keyboardDriver.GamepadsIds;
|
||||
|
||||
if (keyboardIds.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string keyboardId = keyboardIds[0];
|
||||
|
||||
using IGamepad keyboard = _keyboardDriver.GetGamepad(keyboardId);
|
||||
|
||||
if (keyboard == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
fallbackConfig = InputConfigDefaults.CreateDefaultKeyboardConfiguration(
|
||||
keyboardId,
|
||||
keyboard.Name,
|
||||
inputConfig.ControllerType,
|
||||
inputConfig.PlayerIndex);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void ClearInputDriverStates()
|
||||
{
|
||||
foreach (InputConfig inputConfig in _inputConfig)
|
||||
{
|
||||
_controllers[(int)inputConfig.PlayerIndex]?.GamepadDriver?.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void UnblockInputUpdates()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (InputConfig inputConfig in _inputConfig)
|
||||
if (_inputUpdateBlockCount == 0)
|
||||
{
|
||||
_controllers[(int)inputConfig.PlayerIndex]?.GamepadDriver?.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
_blockInputUpdates = false;
|
||||
_inputUpdateBlockCount--;
|
||||
|
||||
if (_inputUpdateBlockCount == 0)
|
||||
{
|
||||
ClearInputDriverStates();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +244,7 @@ namespace Ryujinx.Input.HLE
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
return _blockInputUpdates;
|
||||
return _inputUpdateBlockCount > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,7 +252,7 @@ namespace Ryujinx.Input.HLE
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_blockInputUpdates = true;
|
||||
_inputUpdateBlockCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +284,7 @@ namespace Ryujinx.Input.HLE
|
||||
bool isJoyconPair = false;
|
||||
|
||||
// Do we allow input updates and is a controller connected?
|
||||
if (!_blockInputUpdates && controller != null)
|
||||
if (_inputUpdateBlockCount == 0 && controller != null)
|
||||
{
|
||||
DriverConfigurationUpdate(ref controller, inputConfig);
|
||||
|
||||
@@ -273,7 +322,7 @@ namespace Ryujinx.Input.HLE
|
||||
}
|
||||
}
|
||||
|
||||
if (!_blockInputUpdates && _enableKeyboard)
|
||||
if (_inputUpdateBlockCount == 0 && _enableKeyboard)
|
||||
{
|
||||
hleKeyboardInput = NpadController.GetHLEKeyboardInput(_keyboardDriver);
|
||||
}
|
||||
@@ -334,7 +383,7 @@ namespace Ryujinx.Input.HLE
|
||||
}
|
||||
}
|
||||
|
||||
internal InputConfig GetPlayerInputConfigByIndex(int index)
|
||||
public InputConfig GetPlayerInputConfigByIndex(int index)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
|
||||
@@ -80,10 +80,7 @@ namespace Ryujinx.Input
|
||||
/// </summary>
|
||||
/// <param name="left">The vibration data for the left side</param>
|
||||
/// <param name="right">The vibration data for the right side</param>
|
||||
bool HDRumble(VibrationValue left, VibrationValue right)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool HDRumble(VibrationValue left, VibrationValue right);
|
||||
|
||||
/// <summary>
|
||||
/// Starts a rumble effect on the gamepad.
|
||||
@@ -91,10 +88,10 @@ namespace Ryujinx.Input
|
||||
/// <param name="lowFrequency">The intensity of the low frequency from 0.0f to 1.0f</param>
|
||||
/// <param name="highFrequency">The intensity of the high frequency from 0.0f to 1.0f</param>
|
||||
/// <param name="durationMs">The duration of the rumble effect in milliseconds.</param>
|
||||
void Rumble(float lowFrequency, float highFrequency, uint durationMs);
|
||||
bool Rumble(float lowFrequency, float highFrequency, uint durationMs);
|
||||
|
||||
/// <summary>
|
||||
/// Get a snaphost of the state of the gamepad that is remapped with the informations from the <see cref="InputConfig"/> set via <see cref="SetConfiguration(InputConfig)"/>.
|
||||
/// Get a snaphost of the state of the gamepad that is remapped with the information from the <see cref="InputConfig"/> set via <see cref="SetConfiguration(InputConfig)"/>.
|
||||
/// </summary>
|
||||
/// <returns>A remapped snaphost of the state of the gamepad.</returns>
|
||||
GamepadStateSnapshot GetMappedStateSnapshot();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Buffers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
@@ -33,15 +34,26 @@ namespace Ryujinx.Input
|
||||
{
|
||||
if (_keyState is null)
|
||||
{
|
||||
_keyState = new bool[(int)Key.Count];
|
||||
_keyState = new bool[(int)ConfigPhysicalKey.Count];
|
||||
}
|
||||
|
||||
for (Key key = 0; key < Key.Count; key++)
|
||||
for (ConfigPhysicalKey key = 0; key < ConfigPhysicalKey.Count; key++)
|
||||
{
|
||||
_keyState[(int)key] = keyboard.IsPressed(key);
|
||||
_keyState[(int)key] = keyboard.IsPressed((Key)(int)key);
|
||||
}
|
||||
|
||||
return new KeyboardStateSnapshot(_keyState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to consume a recently pressed key.
|
||||
/// </summary>
|
||||
/// <param name="key">The pressed key, if available.</param>
|
||||
/// <returns>True if a key press was consumed.</returns>
|
||||
bool TryConsumePressedKey(out Key key)
|
||||
{
|
||||
key = Key.Unknown;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
7
src/Ryujinx.Input/IKeyboardModeDriver.cs
Normal file
7
src/Ryujinx.Input/IKeyboardModeDriver.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
public interface IKeyboardModeDriver : IGamepadDriver
|
||||
{
|
||||
IKeyboard GetKeyboard(string id, KeyboardInputMode mode);
|
||||
}
|
||||
}
|
||||
148
src/Ryujinx.Input/InputConfigDefaults.cs
Normal file
148
src/Ryujinx.Input/InputConfigDefaults.cs
Normal file
@@ -0,0 +1,148 @@
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
|
||||
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
|
||||
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
|
||||
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
public static class InputConfigDefaults
|
||||
{
|
||||
public static StandardKeyboardInputConfig CreateDefaultKeyboardConfiguration(
|
||||
string id,
|
||||
string name,
|
||||
ControllerType controllerType,
|
||||
PlayerIndex playerIndex)
|
||||
{
|
||||
return new StandardKeyboardInputConfig
|
||||
{
|
||||
Version = InputConfig.CurrentVersion,
|
||||
Backend = InputBackendType.WindowKeyboard,
|
||||
Id = id,
|
||||
Name = name,
|
||||
PlayerIndex = playerIndex,
|
||||
ControllerType = controllerType,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<ConfigPhysicalKey>
|
||||
{
|
||||
DpadUp = ConfigPhysicalKey.Up,
|
||||
DpadDown = ConfigPhysicalKey.Down,
|
||||
DpadLeft = ConfigPhysicalKey.Left,
|
||||
DpadRight = ConfigPhysicalKey.Right,
|
||||
ButtonMinus = ConfigPhysicalKey.Minus,
|
||||
ButtonL = ConfigPhysicalKey.E,
|
||||
ButtonZl = ConfigPhysicalKey.Q,
|
||||
ButtonSl = ConfigPhysicalKey.Unbound,
|
||||
ButtonSr = ConfigPhysicalKey.Unbound,
|
||||
},
|
||||
LeftJoyconStick = new JoyconConfigKeyboardStick<ConfigPhysicalKey>
|
||||
{
|
||||
StickUp = ConfigPhysicalKey.W,
|
||||
StickDown = ConfigPhysicalKey.S,
|
||||
StickLeft = ConfigPhysicalKey.A,
|
||||
StickRight = ConfigPhysicalKey.D,
|
||||
StickButton = ConfigPhysicalKey.F,
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<ConfigPhysicalKey>
|
||||
{
|
||||
ButtonA = ConfigPhysicalKey.Z,
|
||||
ButtonB = ConfigPhysicalKey.X,
|
||||
ButtonX = ConfigPhysicalKey.C,
|
||||
ButtonY = ConfigPhysicalKey.V,
|
||||
ButtonPlus = ConfigPhysicalKey.Plus,
|
||||
ButtonR = ConfigPhysicalKey.U,
|
||||
ButtonZr = ConfigPhysicalKey.O,
|
||||
ButtonSl = ConfigPhysicalKey.Unbound,
|
||||
ButtonSr = ConfigPhysicalKey.Unbound,
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigKeyboardStick<ConfigPhysicalKey>
|
||||
{
|
||||
StickUp = ConfigPhysicalKey.I,
|
||||
StickDown = ConfigPhysicalKey.K,
|
||||
StickLeft = ConfigPhysicalKey.J,
|
||||
StickRight = ConfigPhysicalKey.L,
|
||||
StickButton = ConfigPhysicalKey.H,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static StandardControllerInputConfig CreateDefaultControllerConfiguration(
|
||||
string id,
|
||||
string name,
|
||||
ControllerType controllerType,
|
||||
PlayerIndex playerIndex,
|
||||
bool isNintendoStyle)
|
||||
{
|
||||
return new StandardControllerInputConfig
|
||||
{
|
||||
Version = InputConfig.CurrentVersion,
|
||||
Backend = InputBackendType.GamepadSDL3,
|
||||
Id = id,
|
||||
Name = name,
|
||||
PlayerIndex = playerIndex,
|
||||
ControllerType = controllerType,
|
||||
DeadzoneLeft = 0.1f,
|
||||
DeadzoneRight = 0.1f,
|
||||
RangeLeft = 1.0f,
|
||||
RangeRight = 1.0f,
|
||||
TriggerThreshold = 0.5f,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId>
|
||||
{
|
||||
DpadUp = ConfigGamepadInputId.DpadUp,
|
||||
DpadDown = ConfigGamepadInputId.DpadDown,
|
||||
DpadLeft = ConfigGamepadInputId.DpadLeft,
|
||||
DpadRight = ConfigGamepadInputId.DpadRight,
|
||||
ButtonMinus = ConfigGamepadInputId.Minus,
|
||||
ButtonL = ConfigGamepadInputId.LeftShoulder,
|
||||
ButtonZl = ConfigGamepadInputId.LeftTrigger,
|
||||
ButtonSl = ConfigGamepadInputId.SingleLeftTrigger0,
|
||||
ButtonSr = ConfigGamepadInputId.SingleRightTrigger0,
|
||||
},
|
||||
LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
|
||||
{
|
||||
Joystick = ConfigStickInputId.Left,
|
||||
StickButton = ConfigGamepadInputId.LeftStick,
|
||||
InvertStickX = false,
|
||||
InvertStickY = false,
|
||||
Rotate90CW = false,
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId>
|
||||
{
|
||||
ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B,
|
||||
ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A,
|
||||
ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y,
|
||||
ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X,
|
||||
ButtonPlus = ConfigGamepadInputId.Plus,
|
||||
ButtonR = ConfigGamepadInputId.RightShoulder,
|
||||
ButtonZr = ConfigGamepadInputId.RightTrigger,
|
||||
ButtonSl = ConfigGamepadInputId.SingleLeftTrigger1,
|
||||
ButtonSr = ConfigGamepadInputId.SingleRightTrigger1,
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
|
||||
{
|
||||
Joystick = ConfigStickInputId.Right,
|
||||
StickButton = ConfigGamepadInputId.RightStick,
|
||||
InvertStickX = false,
|
||||
InvertStickY = false,
|
||||
Rotate90CW = false,
|
||||
},
|
||||
Motion = new StandardMotionConfigController
|
||||
{
|
||||
MotionBackend = MotionInputBackendType.GamepadDriver,
|
||||
EnableMotion = true,
|
||||
Sensitivity = 100,
|
||||
GyroDeadzone = 1,
|
||||
},
|
||||
Rumble = new RumbleConfigController
|
||||
{
|
||||
StrongRumble = 1f,
|
||||
WeakRumble = 1f,
|
||||
EnableRumble = false,
|
||||
UseHDRumble = true,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
78
src/Ryujinx.Input/KeyboardInputMappingHelper.cs
Normal file
78
src/Ryujinx.Input/KeyboardInputMappingHelper.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using System.Numerics;
|
||||
|
||||
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
public static class KeyboardInputMappingHelper
|
||||
{
|
||||
public readonly record struct KeyboardButtonMapping(GamepadButtonInputId To, ConfigPhysicalKey From)
|
||||
{
|
||||
public bool IsValid => To is not GamepadButtonInputId.Unbound && From is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound;
|
||||
}
|
||||
|
||||
public static KeyboardButtonMapping[] BuildButtonMappings(StandardKeyboardInputConfig configuration) =>
|
||||
[
|
||||
// Left JoyCon
|
||||
new(GamepadButtonInputId.LeftStick, configuration.LeftJoyconStick.StickButton),
|
||||
new(GamepadButtonInputId.DpadUp, configuration.LeftJoycon.DpadUp),
|
||||
new(GamepadButtonInputId.DpadDown, configuration.LeftJoycon.DpadDown),
|
||||
new(GamepadButtonInputId.DpadLeft, configuration.LeftJoycon.DpadLeft),
|
||||
new(GamepadButtonInputId.DpadRight, configuration.LeftJoycon.DpadRight),
|
||||
new(GamepadButtonInputId.Minus, configuration.LeftJoycon.ButtonMinus),
|
||||
new(GamepadButtonInputId.LeftShoulder, configuration.LeftJoycon.ButtonL),
|
||||
new(GamepadButtonInputId.LeftTrigger, configuration.LeftJoycon.ButtonZl),
|
||||
new(GamepadButtonInputId.SingleRightTrigger0, configuration.LeftJoycon.ButtonSr),
|
||||
new(GamepadButtonInputId.SingleLeftTrigger0, configuration.LeftJoycon.ButtonSl),
|
||||
|
||||
// Right JoyCon
|
||||
new(GamepadButtonInputId.RightStick, configuration.RightJoyconStick.StickButton),
|
||||
new(GamepadButtonInputId.A, configuration.RightJoycon.ButtonA),
|
||||
new(GamepadButtonInputId.B, configuration.RightJoycon.ButtonB),
|
||||
new(GamepadButtonInputId.X, configuration.RightJoycon.ButtonX),
|
||||
new(GamepadButtonInputId.Y, configuration.RightJoycon.ButtonY),
|
||||
new(GamepadButtonInputId.Plus, configuration.RightJoycon.ButtonPlus),
|
||||
new(GamepadButtonInputId.RightShoulder, configuration.RightJoycon.ButtonR),
|
||||
new(GamepadButtonInputId.RightTrigger, configuration.RightJoycon.ButtonZr),
|
||||
new(GamepadButtonInputId.SingleRightTrigger1, configuration.RightJoycon.ButtonSr),
|
||||
new(GamepadButtonInputId.SingleLeftTrigger1, configuration.RightJoycon.ButtonSl),
|
||||
];
|
||||
|
||||
public static (short X, short Y) GetStickValues(ref KeyboardStateSnapshot snapshot, JoyconConfigKeyboardStick<ConfigPhysicalKey> stickConfig)
|
||||
{
|
||||
short stickX = 0;
|
||||
short stickY = 0;
|
||||
|
||||
if (snapshot.IsPressed(stickConfig.StickUp))
|
||||
{
|
||||
stickY += 1;
|
||||
}
|
||||
|
||||
if (snapshot.IsPressed(stickConfig.StickDown))
|
||||
{
|
||||
stickY -= 1;
|
||||
}
|
||||
|
||||
if (snapshot.IsPressed(stickConfig.StickRight))
|
||||
{
|
||||
stickX += 1;
|
||||
}
|
||||
|
||||
if (snapshot.IsPressed(stickConfig.StickLeft))
|
||||
{
|
||||
stickX -= 1;
|
||||
}
|
||||
|
||||
if (stickX == 0 && stickY == 0)
|
||||
{
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
Vector2 stick = Vector2.Normalize(new Vector2(stickX, stickY));
|
||||
|
||||
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/Ryujinx.Input/KeyboardInputMode.cs
Normal file
8
src/Ryujinx.Input/KeyboardInputMode.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
public enum KeyboardInputMode
|
||||
{
|
||||
Semantic,
|
||||
Physical,
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||
|
||||
namespace Ryujinx.Input
|
||||
{
|
||||
@@ -25,5 +26,8 @@ namespace Ryujinx.Input
|
||||
/// <returns>True if the given key is pressed</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool IsPressed(Key key) => KeysState[(int)key];
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool IsPressed(ConfigPhysicalKey key) => KeysState[(int)key];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ using Ryujinx.Ava.Systems.Configuration;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using Ryujinx.Common.Configuration.Hid.Controller.Motion;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Cpu;
|
||||
@@ -17,14 +15,12 @@ using Ryujinx.Graphics.OpenGL;
|
||||
using Ryujinx.Graphics.Vulkan;
|
||||
using Ryujinx.HLE;
|
||||
using Ryujinx.Input;
|
||||
using Ryujinx.Input.SDL3;
|
||||
using Silk.NET.Vulkan;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using ConfigGamepadInputId = Ryujinx.Common.Configuration.Hid.Controller.GamepadInputId;
|
||||
using ConfigStickInputId = Ryujinx.Common.Configuration.Hid.Controller.StickInputId;
|
||||
using Key = Ryujinx.Common.Configuration.Hid.Key;
|
||||
|
||||
namespace Ryujinx.Headless
|
||||
{
|
||||
@@ -91,6 +87,19 @@ namespace Ryujinx.Headless
|
||||
|
||||
string gamepadName = gamepad.Name;
|
||||
|
||||
bool isNintendoStyle = false;
|
||||
|
||||
if (gamepad is SDL3Gamepad sdlGp)
|
||||
{
|
||||
// Nintendo vendor ID is 0x057E
|
||||
isNintendoStyle = sdlGp.VendorId == 0x057E;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to name-based detection
|
||||
isNintendoStyle = gamepadName.Contains("Nintendo", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
gamepad.Dispose();
|
||||
|
||||
InputConfig config;
|
||||
@@ -99,130 +108,21 @@ namespace Ryujinx.Headless
|
||||
{
|
||||
if (isKeyboard)
|
||||
{
|
||||
config = new StandardKeyboardInputConfig
|
||||
{
|
||||
Version = InputConfig.CurrentVersion,
|
||||
Backend = InputBackendType.WindowKeyboard,
|
||||
Id = null,
|
||||
ControllerType = ControllerType.JoyconPair,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
||||
{
|
||||
DpadUp = Key.Up,
|
||||
DpadDown = Key.Down,
|
||||
DpadLeft = Key.Left,
|
||||
DpadRight = Key.Right,
|
||||
ButtonMinus = Key.Minus,
|
||||
ButtonL = Key.E,
|
||||
ButtonZl = Key.Q,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
},
|
||||
|
||||
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = Key.W,
|
||||
StickDown = Key.S,
|
||||
StickLeft = Key.A,
|
||||
StickRight = Key.D,
|
||||
StickButton = Key.F,
|
||||
},
|
||||
|
||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
||||
{
|
||||
ButtonA = Key.Z,
|
||||
ButtonB = Key.X,
|
||||
ButtonX = Key.C,
|
||||
ButtonY = Key.V,
|
||||
ButtonPlus = Key.Plus,
|
||||
ButtonR = Key.U,
|
||||
ButtonZr = Key.O,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
},
|
||||
|
||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
{
|
||||
StickUp = Key.I,
|
||||
StickDown = Key.K,
|
||||
StickLeft = Key.J,
|
||||
StickRight = Key.L,
|
||||
StickButton = Key.H,
|
||||
},
|
||||
};
|
||||
config = InputConfigDefaults.CreateDefaultKeyboardConfiguration(
|
||||
null,
|
||||
null,
|
||||
ControllerType.JoyconPair,
|
||||
index);
|
||||
}
|
||||
else
|
||||
{
|
||||
bool isNintendoStyle = gamepadName.Contains("Nintendo");
|
||||
|
||||
config = new StandardControllerInputConfig
|
||||
{
|
||||
Version = InputConfig.CurrentVersion,
|
||||
Backend = InputBackendType.GamepadSDL3,
|
||||
Id = null,
|
||||
ControllerType = ControllerType.JoyconPair,
|
||||
DeadzoneLeft = 0.1f,
|
||||
DeadzoneRight = 0.1f,
|
||||
RangeLeft = 1.0f,
|
||||
RangeRight = 1.0f,
|
||||
TriggerThreshold = 0.5f,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<ConfigGamepadInputId>
|
||||
{
|
||||
DpadUp = ConfigGamepadInputId.DpadUp,
|
||||
DpadDown = ConfigGamepadInputId.DpadDown,
|
||||
DpadLeft = ConfigGamepadInputId.DpadLeft,
|
||||
DpadRight = ConfigGamepadInputId.DpadRight,
|
||||
ButtonMinus = ConfigGamepadInputId.Minus,
|
||||
ButtonL = ConfigGamepadInputId.LeftShoulder,
|
||||
ButtonZl = ConfigGamepadInputId.LeftTrigger,
|
||||
ButtonSl = ConfigGamepadInputId.SingleLeftTrigger0,
|
||||
ButtonSr = ConfigGamepadInputId.SingleRightTrigger0,
|
||||
},
|
||||
|
||||
LeftJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
|
||||
{
|
||||
Joystick = ConfigStickInputId.Left,
|
||||
StickButton = ConfigGamepadInputId.LeftStick,
|
||||
InvertStickX = false,
|
||||
InvertStickY = false,
|
||||
Rotate90CW = false,
|
||||
},
|
||||
|
||||
RightJoycon = new RightJoyconCommonConfig<ConfigGamepadInputId>
|
||||
{
|
||||
ButtonA = isNintendoStyle ? ConfigGamepadInputId.A : ConfigGamepadInputId.B,
|
||||
ButtonB = isNintendoStyle ? ConfigGamepadInputId.B : ConfigGamepadInputId.A,
|
||||
ButtonX = isNintendoStyle ? ConfigGamepadInputId.X : ConfigGamepadInputId.Y,
|
||||
ButtonY = isNintendoStyle ? ConfigGamepadInputId.Y : ConfigGamepadInputId.X,
|
||||
ButtonPlus = ConfigGamepadInputId.Plus,
|
||||
ButtonR = ConfigGamepadInputId.RightShoulder,
|
||||
ButtonZr = ConfigGamepadInputId.RightTrigger,
|
||||
ButtonSl = ConfigGamepadInputId.SingleLeftTrigger1,
|
||||
ButtonSr = ConfigGamepadInputId.SingleRightTrigger1,
|
||||
},
|
||||
|
||||
RightJoyconStick = new JoyconConfigControllerStick<ConfigGamepadInputId, ConfigStickInputId>
|
||||
{
|
||||
Joystick = ConfigStickInputId.Right,
|
||||
StickButton = ConfigGamepadInputId.RightStick,
|
||||
InvertStickX = false,
|
||||
InvertStickY = false,
|
||||
Rotate90CW = false,
|
||||
},
|
||||
|
||||
Motion = new StandardMotionConfigController
|
||||
{
|
||||
MotionBackend = MotionInputBackendType.GamepadDriver,
|
||||
EnableMotion = true,
|
||||
Sensitivity = 100,
|
||||
GyroDeadzone = 1,
|
||||
},
|
||||
Rumble = new RumbleConfigController
|
||||
{
|
||||
StrongRumble = 1f,
|
||||
WeakRumble = 1f,
|
||||
EnableRumble = false,
|
||||
},
|
||||
};
|
||||
config = InputConfigDefaults.CreateDefaultControllerConfiguration(
|
||||
null,
|
||||
null,
|
||||
ControllerType.JoyconPair,
|
||||
index,
|
||||
isNintendoStyle);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Configuration.Hid.Keyboard;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.HOS.Services.Hid;
|
||||
using Ryujinx.Input;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Threading;
|
||||
using ConfigKey = Ryujinx.Common.Configuration.Hid.Key;
|
||||
using Key = Ryujinx.Input.Key;
|
||||
|
||||
namespace Ryujinx.Ava.Input
|
||||
{
|
||||
internal class AvaloniaKeyboard : IKeyboard
|
||||
{
|
||||
private readonly List<ButtonMappingEntry> _buttonsUserMapping;
|
||||
private readonly List<KeyboardInputMappingHelper.KeyboardButtonMapping> _buttonsUserMapping;
|
||||
private readonly AvaloniaKeyboardDriver _driver;
|
||||
private readonly KeyboardInputMode _mode;
|
||||
private StandardKeyboardInputConfig _configuration;
|
||||
|
||||
private readonly Lock _userMappingLock = new();
|
||||
@@ -24,18 +25,12 @@ namespace Ryujinx.Ava.Input
|
||||
|
||||
public bool IsConnected => true;
|
||||
public GamepadFeaturesFlag Features => GamepadFeaturesFlag.None;
|
||||
|
||||
private class ButtonMappingEntry(GamepadButtonInputId to, Key from)
|
||||
{
|
||||
public readonly GamepadButtonInputId To = to;
|
||||
public readonly Key From = from;
|
||||
}
|
||||
|
||||
public AvaloniaKeyboard(AvaloniaKeyboardDriver driver, string id, string name)
|
||||
public AvaloniaKeyboard(AvaloniaKeyboardDriver driver, string id, string name, KeyboardInputMode mode)
|
||||
{
|
||||
_buttonsUserMapping = [];
|
||||
|
||||
_driver = driver;
|
||||
_mode = mode;
|
||||
Id = id;
|
||||
Name = name;
|
||||
}
|
||||
@@ -57,22 +52,18 @@ namespace Ryujinx.Ava.Input
|
||||
return result;
|
||||
}
|
||||
|
||||
foreach (ButtonMappingEntry entry in _buttonsUserMapping)
|
||||
foreach (KeyboardInputMappingHelper.KeyboardButtonMapping entry in _buttonsUserMapping)
|
||||
{
|
||||
if (entry.From == Key.Unknown || entry.From == Key.Unbound || entry.To == GamepadButtonInputId.Unbound)
|
||||
if (!entry.IsValid || result.IsPressed(entry.To))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// NOTE: Do not touch state of the button already pressed.
|
||||
if (!result.IsPressed(entry.To))
|
||||
{
|
||||
result.SetPressed(entry.To, rawState.IsPressed(entry.From));
|
||||
}
|
||||
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);
|
||||
(short leftStickX, short leftStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.LeftJoyconStick);
|
||||
(short rightStickX, short rightStickY) = KeyboardInputMappingHelper.GetStickValues(ref rawState, _configuration.RightJoyconStick);
|
||||
|
||||
result.SetStick(StickInputId.Left, ConvertRawStickValue(leftStickX), ConvertRawStickValue(leftStickY));
|
||||
result.SetStick(StickInputId.Right, ConvertRawStickValue(rightStickX), ConvertRawStickValue(rightStickY));
|
||||
@@ -100,7 +91,7 @@ namespace Ryujinx.Ava.Input
|
||||
{
|
||||
try
|
||||
{
|
||||
return _driver.IsPressed(key);
|
||||
return _driver.IsPressed(key, _mode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -108,6 +99,19 @@ namespace Ryujinx.Ava.Input
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryConsumePressedKey(out Key key)
|
||||
{
|
||||
try
|
||||
{
|
||||
return _driver.TryConsumePressedKey(_mode, out key);
|
||||
}
|
||||
catch
|
||||
{
|
||||
key = Key.Unknown;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetConfiguration(InputConfig configuration)
|
||||
{
|
||||
lock (_userMappingLock)
|
||||
@@ -116,42 +120,20 @@ namespace Ryujinx.Ava.Input
|
||||
|
||||
_buttonsUserMapping.Clear();
|
||||
|
||||
#pragma warning disable IDE0055 // Disable formatting
|
||||
// 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));
|
||||
|
||||
// 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));
|
||||
#pragma warning restore IDE0055
|
||||
_buttonsUserMapping.AddRange(KeyboardInputMappingHelper.BuildButtonMappings(_configuration));
|
||||
}
|
||||
}
|
||||
|
||||
public void SetLed(uint packedRgb)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard");
|
||||
Logger.Debug?.Print(LogClass.UI, "SetLed called on an AvaloniaKeyboard");
|
||||
}
|
||||
|
||||
public bool HDRumble(VibrationValue left, VibrationValue right) => false;
|
||||
|
||||
public void SetTriggerThreshold(float triggerThreshold) { }
|
||||
|
||||
public void Rumble(float lowFrequency, float highFrequency, uint durationMs) { }
|
||||
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs) => false;
|
||||
|
||||
public Vector3 GetMotionData(MotionInputId inputId) => Vector3.Zero;
|
||||
|
||||
@@ -162,41 +144,9 @@ namespace Ryujinx.Ava.Input
|
||||
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 = new(stickX, stickY);
|
||||
|
||||
stick = Vector2.Normalize(stick);
|
||||
|
||||
return ((short)(stick.X * short.MaxValue), (short)(stick.Y * short.MaxValue));
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_driver?.Clear();
|
||||
_driver?.Clear(_mode);
|
||||
}
|
||||
|
||||
public void Dispose() { }
|
||||
|
||||
@@ -1,19 +1,55 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.Systems.Configuration;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Input;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using AvaKey = Avalonia.Input.Key;
|
||||
using ConfigPhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||
using Key = Ryujinx.Input.Key;
|
||||
|
||||
namespace Ryujinx.Ava.Input
|
||||
{
|
||||
internal class AvaloniaKeyboardDriver : IGamepadDriver
|
||||
internal class AvaloniaKeyboardDriver : IKeyboardModeDriver
|
||||
{
|
||||
private enum PhysicalKeySource
|
||||
{
|
||||
Direct,
|
||||
ObservedFallback,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
private enum CGEventFlags : ulong
|
||||
{
|
||||
AlphaShift = 1UL << 16 // CapsLock
|
||||
}
|
||||
|
||||
private enum CGEventSourceStateID : uint
|
||||
{
|
||||
HIDSystemState = 1
|
||||
}
|
||||
|
||||
[DllImport("/System/Library/Frameworks/CoreGraphics.framework/CoreGraphics")]
|
||||
private static extern CGEventFlags CGEventSourceFlagsState(CGEventSourceStateID stateID);
|
||||
private static readonly string[] _keyboardIdentifers = ["0"];
|
||||
private readonly Control _control;
|
||||
private readonly HashSet<AvaKey> _pressedKeys;
|
||||
private readonly Window _window;
|
||||
private readonly HashSet<Key> _semanticPressedKeys;
|
||||
private readonly HashSet<ConfigPhysicalKey> _physicalPressedKeys;
|
||||
private readonly HashSet<Key> _keysToRestoreAfterActivation;
|
||||
private readonly Dictionary<Key, ConfigPhysicalKey> _observedPhysicalKeysBySemanticKey;
|
||||
private readonly Queue<Key> _semanticPressedKeyQueue;
|
||||
private readonly Queue<Key> _physicalPressedKeyQueue;
|
||||
private readonly Lock _pressedKeyQueueLock;
|
||||
private readonly KeyboardInputMode _defaultMode;
|
||||
|
||||
public event EventHandler<KeyEventArgs> KeyPressed;
|
||||
public event EventHandler<KeyEventArgs> KeyRelease;
|
||||
@@ -22,14 +58,41 @@ namespace Ryujinx.Ava.Input
|
||||
public string DriverName => "AvaloniaKeyboardDriver";
|
||||
public ReadOnlySpan<string> GamepadsIds => _keyboardIdentifers;
|
||||
|
||||
public AvaloniaKeyboardDriver(Control control)
|
||||
public AvaloniaKeyboardDriver(Control control, KeyboardInputMode defaultMode = KeyboardInputMode.Semantic)
|
||||
{
|
||||
_control = control;
|
||||
_pressedKeys = [];
|
||||
_window = control as Window ?? TopLevel.GetTopLevel(control) as Window;
|
||||
_semanticPressedKeys = [];
|
||||
_physicalPressedKeys = [];
|
||||
_keysToRestoreAfterActivation = [];
|
||||
_observedPhysicalKeysBySemanticKey = [];
|
||||
_semanticPressedKeyQueue = [];
|
||||
_physicalPressedKeyQueue = [];
|
||||
_pressedKeyQueueLock = new();
|
||||
_defaultMode = defaultMode;
|
||||
|
||||
_control.KeyDown += OnKeyPress;
|
||||
_control.KeyUp += OnKeyRelease;
|
||||
_control.AddHandler(InputElement.KeyDownEvent, OnKeyPress, RoutingStrategies.Tunnel, true);
|
||||
_control.AddHandler(InputElement.KeyUpEvent, OnKeyRelease, RoutingStrategies.Tunnel, true);
|
||||
_control.TextInput += Control_TextInput;
|
||||
_window?.Activated += Window_Activated;
|
||||
_window?.Deactivated += Window_Deactivated;
|
||||
}
|
||||
|
||||
private void Window_Activated(object sender, EventArgs e)
|
||||
{
|
||||
RestorePressedKeysAfterActivation();
|
||||
}
|
||||
|
||||
private void Window_Deactivated(object sender, EventArgs e)
|
||||
{
|
||||
lock (_pressedKeyQueueLock)
|
||||
{
|
||||
_keysToRestoreAfterActivation.Clear();
|
||||
_keysToRestoreAfterActivation.UnionWith(_semanticPressedKeys);
|
||||
_observedPhysicalKeysBySemanticKey.Clear();
|
||||
}
|
||||
|
||||
Clear();
|
||||
}
|
||||
|
||||
private void Control_TextInput(object sender, TextInputEventArgs e)
|
||||
@@ -50,13 +113,18 @@ namespace Ryujinx.Ava.Input
|
||||
}
|
||||
|
||||
public IGamepad GetGamepad(string id)
|
||||
{
|
||||
return GetKeyboard(id, _defaultMode);
|
||||
}
|
||||
|
||||
public IKeyboard GetKeyboard(string id, KeyboardInputMode mode)
|
||||
{
|
||||
if (!_keyboardIdentifers[0].Equals(id))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new AvaloniaKeyboard(this, _keyboardIdentifers[0], LocaleManager.Instance[LocaleKeys.AllKeyboards]);
|
||||
return new AvaloniaKeyboard(this, _keyboardIdentifers[0], LocaleManager.Instance[LocaleKeys.KeyboardLayout_KeyboardInputMode], mode);
|
||||
}
|
||||
|
||||
public IEnumerable<IGamepad> GetGamepads() => [GetGamepad("0")];
|
||||
@@ -65,40 +133,448 @@ namespace Ryujinx.Ava.Input
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_control.KeyUp -= OnKeyPress;
|
||||
_control.KeyDown -= OnKeyRelease;
|
||||
_control.RemoveHandler(InputElement.KeyDownEvent, OnKeyPress);
|
||||
_control.RemoveHandler(InputElement.KeyUpEvent, OnKeyRelease);
|
||||
_control.TextInput -= Control_TextInput;
|
||||
if (_window != null)
|
||||
{
|
||||
_window.Activated -= Window_Activated;
|
||||
_window.Deactivated -= Window_Deactivated;
|
||||
}
|
||||
_observedPhysicalKeysBySemanticKey.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
protected void OnKeyPress(object sender, KeyEventArgs args)
|
||||
{
|
||||
_pressedKeys.Add(args.Key);
|
||||
|
||||
UpdateKeyStates(args, true);
|
||||
KeyPressed?.Invoke(this, args);
|
||||
}
|
||||
|
||||
protected void OnKeyRelease(object sender, KeyEventArgs args)
|
||||
{
|
||||
_pressedKeys.Remove(args.Key);
|
||||
|
||||
UpdateKeyStates(args, false);
|
||||
KeyRelease?.Invoke(this, args);
|
||||
}
|
||||
|
||||
internal bool IsPressed(Key key)
|
||||
internal bool IsPressed(Key key, KeyboardInputMode mode)
|
||||
{
|
||||
if (key is Key.Unbound or Key.Unknown)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
AvaloniaKeyboardMappingHelper.TryGetAvaKey(key, out AvaKey nativeKey);
|
||||
if (key == Key.CapsLock)
|
||||
{
|
||||
return IsCapsLockOnMacOS();
|
||||
}
|
||||
|
||||
return _pressedKeys.Contains(nativeKey);
|
||||
return mode == KeyboardInputMode.Physical
|
||||
? _physicalPressedKeys.Contains((ConfigPhysicalKey)(int)key)
|
||||
: _semanticPressedKeys.Contains(key);
|
||||
}
|
||||
|
||||
private bool IsCapsLockOnMacOS()
|
||||
{
|
||||
bool currentState = false;
|
||||
|
||||
try
|
||||
{
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
CGEventFlags flags = CGEventSourceFlagsState(CGEventSourceStateID.HIDSystemState);
|
||||
currentState = (flags & CGEventFlags.AlphaShift) != 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: use Avalonia's tracked key state (semantic CapsLock)
|
||||
if (AvaloniaKeyboardMappingHelper.TryGetAvaKey(Key.CapsLock, out AvaKey nativeKey))
|
||||
{
|
||||
currentState = _semanticPressedKeys.Contains(Key.CapsLock);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Debug?.Print(LogClass.UI, $"Failed to query CapsLock state: {ex}");
|
||||
}
|
||||
|
||||
return currentState;
|
||||
}
|
||||
|
||||
internal void Clear(KeyboardInputMode mode)
|
||||
{
|
||||
lock (_pressedKeyQueueLock)
|
||||
{
|
||||
if (mode == KeyboardInputMode.Physical)
|
||||
{
|
||||
_physicalPressedKeys.Clear();
|
||||
_physicalPressedKeyQueue.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
_semanticPressedKeys.Clear();
|
||||
_semanticPressedKeyQueue.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_pressedKeys.Clear();
|
||||
lock (_pressedKeyQueueLock)
|
||||
{
|
||||
_semanticPressedKeys.Clear();
|
||||
_physicalPressedKeys.Clear();
|
||||
_semanticPressedKeyQueue.Clear();
|
||||
_physicalPressedKeyQueue.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void RestorePressedKeysAfterActivation()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
lock (_pressedKeyQueueLock)
|
||||
{
|
||||
_keysToRestoreAfterActivation.Clear();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_pressedKeyQueueLock)
|
||||
{
|
||||
if (_keysToRestoreAfterActivation.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Key key in _keysToRestoreAfterActivation)
|
||||
{
|
||||
if (!TryGetWindowsVirtualKey(key, out int virtualKey) ||
|
||||
!IsWindowsKeyPressed(virtualKey))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_semanticPressedKeys.Add(key);
|
||||
|
||||
ConfigPhysicalKey physicalKey = GetPhysicalKeyForSemanticKey(key);
|
||||
|
||||
if (physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound)
|
||||
{
|
||||
_physicalPressedKeys.Add(physicalKey);
|
||||
}
|
||||
}
|
||||
|
||||
_keysToRestoreAfterActivation.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private ConfigPhysicalKey GetPhysicalKeyForSemanticKey(Key key)
|
||||
{
|
||||
if (_observedPhysicalKeysBySemanticKey.TryGetValue(key, out ConfigPhysicalKey physicalKey))
|
||||
{
|
||||
return physicalKey;
|
||||
}
|
||||
|
||||
return key is >= Key.Unknown and < Key.Count
|
||||
? (ConfigPhysicalKey)(int)key
|
||||
: ConfigPhysicalKey.Unknown;
|
||||
}
|
||||
|
||||
[SupportedOSPlatform("windows")]
|
||||
private static bool IsWindowsKeyPressed(int virtualKey)
|
||||
{
|
||||
return (Win32NativeInterop.GetAsyncKeyState(virtualKey) & 0x8000) != 0;
|
||||
}
|
||||
|
||||
private static bool TryGetWindowsVirtualKey(Key key, out int virtualKey)
|
||||
{
|
||||
switch (key)
|
||||
{
|
||||
case >= Key.A and <= Key.Z:
|
||||
virtualKey = 'A' + (int)(key - Key.A);
|
||||
return true;
|
||||
case >= Key.Number0 and <= Key.Number9:
|
||||
virtualKey = '0' + (int)(key - Key.Number0);
|
||||
return true;
|
||||
case >= Key.F1 and <= Key.F24:
|
||||
virtualKey = 0x70 + (int)(key - Key.F1);
|
||||
return true;
|
||||
case Key.ShiftLeft:
|
||||
virtualKey = 0xA0;
|
||||
return true;
|
||||
case Key.ShiftRight:
|
||||
virtualKey = 0xA1;
|
||||
return true;
|
||||
case Key.ControlLeft:
|
||||
virtualKey = 0xA2;
|
||||
return true;
|
||||
case Key.ControlRight:
|
||||
virtualKey = 0xA3;
|
||||
return true;
|
||||
case Key.AltLeft:
|
||||
virtualKey = 0xA4;
|
||||
return true;
|
||||
case Key.AltRight:
|
||||
virtualKey = 0xA5;
|
||||
return true;
|
||||
case Key.WinLeft:
|
||||
virtualKey = 0x5B;
|
||||
return true;
|
||||
case Key.WinRight:
|
||||
virtualKey = 0x5C;
|
||||
return true;
|
||||
case Key.Menu:
|
||||
virtualKey = 0x5D;
|
||||
return true;
|
||||
case Key.Up:
|
||||
virtualKey = 0x26;
|
||||
return true;
|
||||
case Key.Down:
|
||||
virtualKey = 0x28;
|
||||
return true;
|
||||
case Key.Left:
|
||||
virtualKey = 0x25;
|
||||
return true;
|
||||
case Key.Right:
|
||||
virtualKey = 0x27;
|
||||
return true;
|
||||
case Key.Enter:
|
||||
virtualKey = 0x0D;
|
||||
return true;
|
||||
case Key.Escape:
|
||||
virtualKey = 0x1B;
|
||||
return true;
|
||||
case Key.Space:
|
||||
virtualKey = 0x20;
|
||||
return true;
|
||||
case Key.Tab:
|
||||
virtualKey = 0x09;
|
||||
return true;
|
||||
case Key.BackSpace:
|
||||
virtualKey = 0x08;
|
||||
return true;
|
||||
case Key.Insert:
|
||||
virtualKey = 0x2D;
|
||||
return true;
|
||||
case Key.Delete:
|
||||
virtualKey = 0x2E;
|
||||
return true;
|
||||
case Key.PageUp:
|
||||
virtualKey = 0x21;
|
||||
return true;
|
||||
case Key.PageDown:
|
||||
virtualKey = 0x22;
|
||||
return true;
|
||||
case Key.Home:
|
||||
virtualKey = 0x24;
|
||||
return true;
|
||||
case Key.End:
|
||||
virtualKey = 0x23;
|
||||
return true;
|
||||
case Key.CapsLock:
|
||||
virtualKey = 0x14;
|
||||
return true;
|
||||
case Key.ScrollLock:
|
||||
virtualKey = 0x91;
|
||||
return true;
|
||||
case Key.PrintScreen:
|
||||
virtualKey = 0x2C;
|
||||
return true;
|
||||
case Key.Pause:
|
||||
virtualKey = 0x13;
|
||||
return true;
|
||||
case Key.NumLock:
|
||||
virtualKey = 0x90;
|
||||
return true;
|
||||
case Key.Clear:
|
||||
virtualKey = 0x0C;
|
||||
return true;
|
||||
case >= Key.Keypad0 and <= Key.Keypad9:
|
||||
virtualKey = 0x60 + (int)(key - Key.Keypad0);
|
||||
return true;
|
||||
case Key.KeypadDivide:
|
||||
virtualKey = 0x6F;
|
||||
return true;
|
||||
case Key.KeypadMultiply:
|
||||
virtualKey = 0x6A;
|
||||
return true;
|
||||
case Key.KeypadSubtract:
|
||||
virtualKey = 0x6D;
|
||||
return true;
|
||||
case Key.KeypadAdd:
|
||||
virtualKey = 0x6B;
|
||||
return true;
|
||||
case Key.KeypadDecimal:
|
||||
virtualKey = 0x6E;
|
||||
return true;
|
||||
case Key.KeypadEnter:
|
||||
virtualKey = 0x0D;
|
||||
return true;
|
||||
case Key.Tilde:
|
||||
virtualKey = 0xC0;
|
||||
return true;
|
||||
case Key.Grave:
|
||||
virtualKey = 0xE2;
|
||||
return true;
|
||||
case Key.Minus:
|
||||
virtualKey = 0xBD;
|
||||
return true;
|
||||
case Key.Plus:
|
||||
virtualKey = 0xBB;
|
||||
return true;
|
||||
case Key.BracketLeft:
|
||||
virtualKey = 0xDB;
|
||||
return true;
|
||||
case Key.BracketRight:
|
||||
virtualKey = 0xDD;
|
||||
return true;
|
||||
case Key.Semicolon:
|
||||
virtualKey = 0xBA;
|
||||
return true;
|
||||
case Key.Quote:
|
||||
virtualKey = 0xDE;
|
||||
return true;
|
||||
case Key.Comma:
|
||||
virtualKey = 0xBC;
|
||||
return true;
|
||||
case Key.Period:
|
||||
virtualKey = 0xBE;
|
||||
return true;
|
||||
case Key.Slash:
|
||||
virtualKey = 0xBF;
|
||||
return true;
|
||||
case Key.BackSlash:
|
||||
virtualKey = 0xDC;
|
||||
return true;
|
||||
default:
|
||||
virtualKey = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal bool TryConsumePressedKey(KeyboardInputMode mode, out Key key)
|
||||
{
|
||||
lock (_pressedKeyQueueLock)
|
||||
{
|
||||
Queue<Key> queue = mode == KeyboardInputMode.Physical ? _physicalPressedKeyQueue : _semanticPressedKeyQueue;
|
||||
|
||||
if (queue.TryDequeue(out key))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
key = Key.Unknown;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static void UpdateKeyState(HashSet<Key> pressedKeys, Key key, bool isPressed)
|
||||
{
|
||||
if (key is Key.Unknown or Key.Unbound)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPressed)
|
||||
{
|
||||
pressedKeys.Add(key);
|
||||
return;
|
||||
}
|
||||
|
||||
pressedKeys.Remove(key);
|
||||
}
|
||||
|
||||
private static void UpdateKeyState(HashSet<ConfigPhysicalKey> pressedKeys, ConfigPhysicalKey key, bool isPressed)
|
||||
{
|
||||
if (key is ConfigPhysicalKey.Unknown or ConfigPhysicalKey.Unbound)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPressed)
|
||||
{
|
||||
pressedKeys.Add(key);
|
||||
return;
|
||||
}
|
||||
|
||||
pressedKeys.Remove(key);
|
||||
}
|
||||
|
||||
private void UpdateKeyStates(KeyEventArgs args, bool isPressed)
|
||||
{
|
||||
Key semanticKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.Key);
|
||||
Key resolvedSemanticKey = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey, args.Key);
|
||||
ConfigPhysicalKey physicalKey = GetPhysicalInputKey(args, semanticKey, out PhysicalKeySource physicalKeySource);
|
||||
bool semanticWasPressed = _semanticPressedKeys.Contains(resolvedSemanticKey);
|
||||
bool physicalWasPressed = _physicalPressedKeys.Contains(physicalKey);
|
||||
bool semanticStateChanged = resolvedSemanticKey is not Key.Unknown and not Key.Unbound && semanticWasPressed != isPressed;
|
||||
bool physicalStateChanged = physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound && physicalWasPressed != isPressed;
|
||||
bool bufferedSemanticPress = false;
|
||||
bool bufferedPhysicalPress = false;
|
||||
|
||||
UpdateKeyState(_semanticPressedKeys, resolvedSemanticKey, isPressed);
|
||||
UpdateKeyState(_physicalPressedKeys, physicalKey, isPressed);
|
||||
|
||||
if (isPressed)
|
||||
{
|
||||
lock (_pressedKeyQueueLock)
|
||||
{
|
||||
if (!semanticWasPressed && resolvedSemanticKey is not Key.Unknown and not Key.Unbound)
|
||||
{
|
||||
_semanticPressedKeyQueue.Enqueue(resolvedSemanticKey);
|
||||
bufferedSemanticPress = true;
|
||||
}
|
||||
|
||||
if (!physicalWasPressed && physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound)
|
||||
{
|
||||
_physicalPressedKeyQueue.Enqueue((Key)(int)physicalKey);
|
||||
bufferedPhysicalPress = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isPressed &&
|
||||
semanticKey is not Key.Unknown and not Key.Unbound &&
|
||||
physicalKey is not ConfigPhysicalKey.Unknown and not ConfigPhysicalKey.Unbound)
|
||||
{
|
||||
_observedPhysicalKeysBySemanticKey[semanticKey] = physicalKey;
|
||||
}
|
||||
|
||||
if (ConfigurationState.Instance.Logger.EnableAvaloniaLog &&
|
||||
(semanticStateChanged || physicalStateChanged))
|
||||
{
|
||||
Logger.Info?.Print(
|
||||
LogClass.UI,
|
||||
$"Keyboard {(isPressed ? "down" : "up")}: avaloniaKey={args.Key}, avaloniaPhysical={args.PhysicalKey}, keySymbol={FormatKeySymbol(args.KeySymbol)}, modifiers={args.KeyModifiers}, semantic={semanticKey}, resolvedSemantic={resolvedSemanticKey}, physical={physicalKey}, physicalSource={physicalKeySource}, bufferedSemantic={bufferedSemanticPress}, bufferedPhysical={bufferedPhysicalPress}, semanticPressed={_semanticPressedKeys.Count}, physicalPressed={_physicalPressedKeys.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
private ConfigPhysicalKey GetPhysicalInputKey(KeyEventArgs args, Key semanticKey, out PhysicalKeySource source)
|
||||
{
|
||||
Key key = AvaloniaKeyboardMappingHelper.ToInputKey(args.PhysicalKey);
|
||||
|
||||
if (key is >= Key.Unknown and < Key.Count)
|
||||
{
|
||||
source = PhysicalKeySource.Direct;
|
||||
return (ConfigPhysicalKey)(int)key;
|
||||
}
|
||||
|
||||
if (semanticKey is not Key.Unknown and not Key.Unbound &&
|
||||
_observedPhysicalKeysBySemanticKey.TryGetValue(semanticKey, out ConfigPhysicalKey observedPhysicalKey))
|
||||
{
|
||||
source = PhysicalKeySource.ObservedFallback;
|
||||
return observedPhysicalKey;
|
||||
}
|
||||
|
||||
source = PhysicalKeySource.Unknown;
|
||||
return ConfigPhysicalKey.Unknown;
|
||||
}
|
||||
|
||||
private static string FormatKeySymbol(string keySymbol)
|
||||
{
|
||||
return string.IsNullOrEmpty(keySymbol) ? "<none>" : keySymbol;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -2,6 +2,7 @@ using Ryujinx.Input;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AvaKey = Avalonia.Input.Key;
|
||||
using AvaPhysicalKey = Avalonia.Input.PhysicalKey;
|
||||
|
||||
namespace Ryujinx.Ava.Input
|
||||
{
|
||||
@@ -132,7 +133,8 @@ namespace Ryujinx.Ava.Input
|
||||
AvaKey.D8,
|
||||
AvaKey.D9,
|
||||
AvaKey.OemTilde,
|
||||
AvaKey.OemTilde,AvaKey.OemMinus,
|
||||
AvaKey.Oem102,
|
||||
AvaKey.OemMinus,
|
||||
AvaKey.OemPlus,
|
||||
AvaKey.OemOpenBrackets,
|
||||
AvaKey.OemCloseBrackets,
|
||||
@@ -147,7 +149,149 @@ namespace Ryujinx.Ava.Input
|
||||
AvaKey.None
|
||||
];
|
||||
|
||||
private static readonly AvaPhysicalKey[] _physicalKeyMapping =
|
||||
[
|
||||
// NOTE: Invalid
|
||||
AvaPhysicalKey.None,
|
||||
|
||||
AvaPhysicalKey.ShiftLeft,
|
||||
AvaPhysicalKey.ShiftRight,
|
||||
AvaPhysicalKey.ControlLeft,
|
||||
AvaPhysicalKey.ControlRight,
|
||||
AvaPhysicalKey.AltLeft,
|
||||
AvaPhysicalKey.AltRight,
|
||||
AvaPhysicalKey.MetaLeft,
|
||||
AvaPhysicalKey.MetaRight,
|
||||
AvaPhysicalKey.ContextMenu,
|
||||
AvaPhysicalKey.F1,
|
||||
AvaPhysicalKey.F2,
|
||||
AvaPhysicalKey.F3,
|
||||
AvaPhysicalKey.F4,
|
||||
AvaPhysicalKey.F5,
|
||||
AvaPhysicalKey.F6,
|
||||
AvaPhysicalKey.F7,
|
||||
AvaPhysicalKey.F8,
|
||||
AvaPhysicalKey.F9,
|
||||
AvaPhysicalKey.F10,
|
||||
AvaPhysicalKey.F11,
|
||||
AvaPhysicalKey.F12,
|
||||
AvaPhysicalKey.F13,
|
||||
AvaPhysicalKey.F14,
|
||||
AvaPhysicalKey.F15,
|
||||
AvaPhysicalKey.F16,
|
||||
AvaPhysicalKey.F17,
|
||||
AvaPhysicalKey.F18,
|
||||
AvaPhysicalKey.F19,
|
||||
AvaPhysicalKey.F20,
|
||||
AvaPhysicalKey.F21,
|
||||
AvaPhysicalKey.F22,
|
||||
AvaPhysicalKey.F23,
|
||||
AvaPhysicalKey.F24,
|
||||
|
||||
AvaPhysicalKey.None,
|
||||
AvaPhysicalKey.None,
|
||||
AvaPhysicalKey.None,
|
||||
AvaPhysicalKey.None,
|
||||
AvaPhysicalKey.None,
|
||||
AvaPhysicalKey.None,
|
||||
AvaPhysicalKey.None,
|
||||
AvaPhysicalKey.None,
|
||||
AvaPhysicalKey.None,
|
||||
AvaPhysicalKey.None,
|
||||
AvaPhysicalKey.None,
|
||||
|
||||
AvaPhysicalKey.ArrowUp,
|
||||
AvaPhysicalKey.ArrowDown,
|
||||
AvaPhysicalKey.ArrowLeft,
|
||||
AvaPhysicalKey.ArrowRight,
|
||||
AvaPhysicalKey.Enter,
|
||||
AvaPhysicalKey.Escape,
|
||||
AvaPhysicalKey.Space,
|
||||
AvaPhysicalKey.Tab,
|
||||
AvaPhysicalKey.Backspace,
|
||||
AvaPhysicalKey.Insert,
|
||||
AvaPhysicalKey.Delete,
|
||||
AvaPhysicalKey.PageUp,
|
||||
AvaPhysicalKey.PageDown,
|
||||
AvaPhysicalKey.Home,
|
||||
AvaPhysicalKey.End,
|
||||
AvaPhysicalKey.CapsLock,
|
||||
AvaPhysicalKey.ScrollLock,
|
||||
AvaPhysicalKey.PrintScreen,
|
||||
AvaPhysicalKey.Pause,
|
||||
AvaPhysicalKey.NumLock,
|
||||
AvaPhysicalKey.NumPadClear,
|
||||
AvaPhysicalKey.NumPad0,
|
||||
AvaPhysicalKey.NumPad1,
|
||||
AvaPhysicalKey.NumPad2,
|
||||
AvaPhysicalKey.NumPad3,
|
||||
AvaPhysicalKey.NumPad4,
|
||||
AvaPhysicalKey.NumPad5,
|
||||
AvaPhysicalKey.NumPad6,
|
||||
AvaPhysicalKey.NumPad7,
|
||||
AvaPhysicalKey.NumPad8,
|
||||
AvaPhysicalKey.NumPad9,
|
||||
AvaPhysicalKey.NumPadDivide,
|
||||
AvaPhysicalKey.NumPadMultiply,
|
||||
AvaPhysicalKey.NumPadSubtract,
|
||||
AvaPhysicalKey.NumPadAdd,
|
||||
AvaPhysicalKey.NumPadDecimal,
|
||||
AvaPhysicalKey.NumPadEnter,
|
||||
AvaPhysicalKey.A,
|
||||
AvaPhysicalKey.B,
|
||||
AvaPhysicalKey.C,
|
||||
AvaPhysicalKey.D,
|
||||
AvaPhysicalKey.E,
|
||||
AvaPhysicalKey.F,
|
||||
AvaPhysicalKey.G,
|
||||
AvaPhysicalKey.H,
|
||||
AvaPhysicalKey.I,
|
||||
AvaPhysicalKey.J,
|
||||
AvaPhysicalKey.K,
|
||||
AvaPhysicalKey.L,
|
||||
AvaPhysicalKey.M,
|
||||
AvaPhysicalKey.N,
|
||||
AvaPhysicalKey.O,
|
||||
AvaPhysicalKey.P,
|
||||
AvaPhysicalKey.Q,
|
||||
AvaPhysicalKey.R,
|
||||
AvaPhysicalKey.S,
|
||||
AvaPhysicalKey.T,
|
||||
AvaPhysicalKey.U,
|
||||
AvaPhysicalKey.V,
|
||||
AvaPhysicalKey.W,
|
||||
AvaPhysicalKey.X,
|
||||
AvaPhysicalKey.Y,
|
||||
AvaPhysicalKey.Z,
|
||||
AvaPhysicalKey.Digit0,
|
||||
AvaPhysicalKey.Digit1,
|
||||
AvaPhysicalKey.Digit2,
|
||||
AvaPhysicalKey.Digit3,
|
||||
AvaPhysicalKey.Digit4,
|
||||
AvaPhysicalKey.Digit5,
|
||||
AvaPhysicalKey.Digit6,
|
||||
AvaPhysicalKey.Digit7,
|
||||
AvaPhysicalKey.Digit8,
|
||||
AvaPhysicalKey.Digit9,
|
||||
AvaPhysicalKey.Backquote,
|
||||
AvaPhysicalKey.IntlBackslash,
|
||||
AvaPhysicalKey.Minus,
|
||||
AvaPhysicalKey.Equal,
|
||||
AvaPhysicalKey.BracketLeft,
|
||||
AvaPhysicalKey.BracketRight,
|
||||
AvaPhysicalKey.Semicolon,
|
||||
AvaPhysicalKey.Quote,
|
||||
AvaPhysicalKey.Comma,
|
||||
AvaPhysicalKey.Period,
|
||||
AvaPhysicalKey.Slash,
|
||||
AvaPhysicalKey.Backslash,
|
||||
|
||||
// NOTE: invalid
|
||||
AvaPhysicalKey.None
|
||||
];
|
||||
|
||||
private static readonly Dictionary<AvaKey, Key> _avaKeyMapping;
|
||||
private static readonly Dictionary<AvaPhysicalKey, Key> _avaPhysicalKeyMapping;
|
||||
|
||||
static AvaloniaKeyboardMappingHelper()
|
||||
{
|
||||
@@ -155,21 +299,42 @@ namespace Ryujinx.Ava.Input
|
||||
|
||||
// NOTE: Avalonia.Input.Key is not contiguous and quite large, so use a dictionary instead of an array.
|
||||
_avaKeyMapping = new Dictionary<AvaKey, Key>();
|
||||
_avaPhysicalKeyMapping = new Dictionary<AvaPhysicalKey, Key>();
|
||||
|
||||
foreach (Key key in inputKeys)
|
||||
{
|
||||
if (TryGetAvaKey(key, out AvaKey index))
|
||||
if (TryGetAvaKey(key, out AvaKey avaKey))
|
||||
{
|
||||
_avaKeyMapping[index] = key;
|
||||
_avaKeyMapping[avaKey] = key;
|
||||
}
|
||||
|
||||
if (TryGetAvaPhysicalKey(key, out AvaPhysicalKey avaPhysicalKey))
|
||||
{
|
||||
_avaPhysicalKeyMapping[avaPhysicalKey] = key;
|
||||
}
|
||||
}
|
||||
|
||||
// Alias additional Avalonia key values to improve non-US layout support.
|
||||
_avaKeyMapping[AvaKey.Oem1] = Key.Semicolon;
|
||||
_avaKeyMapping[AvaKey.Oem2] = Key.Slash;
|
||||
_avaKeyMapping[AvaKey.Oem3] = Key.Tilde;
|
||||
_avaKeyMapping[AvaKey.Oem4] = Key.BracketLeft;
|
||||
_avaKeyMapping[AvaKey.Oem5] = Key.BackSlash;
|
||||
_avaKeyMapping[AvaKey.Oem6] = Key.BracketRight;
|
||||
_avaKeyMapping[AvaKey.Oem7] = Key.Quote;
|
||||
_avaKeyMapping[AvaKey.OemBackslash] = Key.Grave;
|
||||
_avaKeyMapping[AvaKey.Oem102] = Key.Grave;
|
||||
|
||||
// Common alternates for non-US/JIS physical keys.
|
||||
_avaPhysicalKeyMapping[AvaPhysicalKey.IntlRo] = Key.BackSlash;
|
||||
_avaPhysicalKeyMapping[AvaPhysicalKey.IntlYen] = Key.BackSlash;
|
||||
}
|
||||
|
||||
public static bool TryGetAvaKey(Key key, out AvaKey avaKey)
|
||||
{
|
||||
avaKey = AvaKey.None;
|
||||
|
||||
bool keyExist = (int)key < _keyMapping.Length;
|
||||
bool keyExist = key < Key.Count && (int)key < _keyMapping.Length;
|
||||
if (keyExist)
|
||||
{
|
||||
avaKey = _keyMapping[(int)key];
|
||||
@@ -178,9 +343,34 @@ namespace Ryujinx.Ava.Input
|
||||
return keyExist;
|
||||
}
|
||||
|
||||
public static bool TryGetAvaPhysicalKey(Key key, out AvaPhysicalKey avaPhysicalKey)
|
||||
{
|
||||
avaPhysicalKey = AvaPhysicalKey.None;
|
||||
|
||||
bool keyExist = key < Key.Count && (int)key < _physicalKeyMapping.Length;
|
||||
if (keyExist)
|
||||
{
|
||||
avaPhysicalKey = _physicalKeyMapping[(int)key];
|
||||
}
|
||||
|
||||
return keyExist;
|
||||
}
|
||||
|
||||
public static Key ToInputKey(AvaKey key)
|
||||
{
|
||||
return _avaKeyMapping.GetValueOrDefault(key, Key.Unknown);
|
||||
}
|
||||
|
||||
public static Key ToInputKey(AvaPhysicalKey key)
|
||||
{
|
||||
return _avaPhysicalKeyMapping.GetValueOrDefault(key, Key.Unknown);
|
||||
}
|
||||
|
||||
public static Key ToInputKey(AvaPhysicalKey physicalKey, AvaKey key)
|
||||
{
|
||||
Key inputKey = ToInputKey(key);
|
||||
|
||||
return inputKey != Key.Unknown ? inputKey : ToInputKey(physicalKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Ryujinx.Common.Configuration.Hid;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.HOS.Services.Hid;
|
||||
using Ryujinx.Input;
|
||||
using System;
|
||||
using System.Drawing;
|
||||
@@ -65,7 +66,12 @@ namespace Ryujinx.Ava.Input
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
||||
public bool HDRumble(VibrationValue left, VibrationValue right)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public bool Rumble(float lowFrequency, float highFrequency, uint durationMs)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace Ryujinx.Ava
|
||||
|
||||
private const uint MbIconwarning = 0x30;
|
||||
|
||||
[STAThread]
|
||||
public static int Main(string[] args)
|
||||
{
|
||||
Version = ReleaseInformation.Version;
|
||||
@@ -54,17 +55,39 @@ namespace Ryujinx.Ava
|
||||
{
|
||||
if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 19041))
|
||||
{
|
||||
_ = Win32NativeInterop.MessageBoxA(nint.Zero, "You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 20H1 and newer.\n", $"Ryujinx {Version}", MbIconwarning);
|
||||
Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run on an outdated version of Windows. Exiting...");
|
||||
_ = Win32NativeInterop.MessageBoxA(nint.Zero,
|
||||
"You are running an outdated version of Windows.\n\nRyujinx supports Windows 10 version 20H1 and newer.\n",
|
||||
$"Ryujinx {Version}", MbIconwarning);
|
||||
return 0;
|
||||
}
|
||||
|
||||
var programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
||||
var programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
|
||||
string onedriveFiles = Environment.GetEnvironmentVariable("Onedrive");
|
||||
string onedriveConsumerFiles = Environment.GetEnvironmentVariable("OnedriveConsumer");
|
||||
string onedriveCommercialFiles = Environment.GetEnvironmentVariable("OnedriveCommercial");
|
||||
|
||||
// Apparently not everyone has OneDrive shoved onto their system.
|
||||
if ((onedriveFiles is not null && Environment.CurrentDirectory.StartsWithIgnoreCase(onedriveFiles))
|
||||
|| (onedriveConsumerFiles is not null && Environment.CurrentDirectory.StartsWithIgnoreCase(onedriveConsumerFiles))
|
||||
|| (onedriveCommercialFiles is not null && Environment.CurrentDirectory.StartsWithIgnoreCase(onedriveCommercialFiles)))
|
||||
{
|
||||
Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from a OneDrive folder. Exiting...");
|
||||
_ = Win32NativeInterop.MessageBoxA(nint.Zero,
|
||||
"Ryujinx is not intended to be run from a OneDrive folder. Please move it out and relaunch.",
|
||||
$"Ryujinx {Version}", MbIconwarning);
|
||||
return 0;
|
||||
}
|
||||
|
||||
string programFiles = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles);
|
||||
string programFilesX86 = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86);
|
||||
|
||||
if (Environment.CurrentDirectory.StartsWithIgnoreCase(programFiles) ||
|
||||
Environment.CurrentDirectory.StartsWithIgnoreCase(programFilesX86))
|
||||
{
|
||||
_ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run from the Program Files folder. Please move it out and relaunch.", $"Ryujinx {Version}", MbIconwarning);
|
||||
Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from the Program Files folder. Exiting...");
|
||||
_ = Win32NativeInterop.MessageBoxA(nint.Zero,
|
||||
"Ryujinx is not intended to be run from the Program Files folder. Please move it out and relaunch.",
|
||||
$"Ryujinx {Version}", MbIconwarning);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -74,10 +97,70 @@ namespace Ryujinx.Ava
|
||||
// ...but this reads like it checks if the current is in/has the Windows admin role? lol
|
||||
if (new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator))
|
||||
{
|
||||
_ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run as administrator.", $"Ryujinx {Version}", MbIconwarning);
|
||||
Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run as administrator. Exiting...");
|
||||
_ = Win32NativeInterop.MessageBoxA(nint.Zero, "Ryujinx is not intended to be run as administrator.",
|
||||
$"Ryujinx {Version}", MbIconwarning);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
else // Unix
|
||||
{
|
||||
// sudo check
|
||||
[DllImport("libc")]
|
||||
static extern uint geteuid();
|
||||
bool root = geteuid().Equals(0);
|
||||
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
if (root)
|
||||
{
|
||||
Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run as administrator. Exiting...");
|
||||
macOSNativeInterop.SimpleMessageBox($"Ryujinx {Version}",
|
||||
"Ryujinx is not intended to be run as administrator.", "Ok");
|
||||
return 0;
|
||||
}
|
||||
|
||||
string downloadFiles = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads");
|
||||
|
||||
if (Environment.CurrentDirectory.StartsWithIgnoreCase(downloadFiles))
|
||||
{
|
||||
Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from the Downloads folder. Exiting...");
|
||||
macOSNativeInterop.SimpleMessageBox($"Ryujinx {Version}",
|
||||
"Ryujinx is not intended to be run from the Downloads folder.", "Ok");
|
||||
return 0;
|
||||
}
|
||||
|
||||
string icloudFiles = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library/Mobile Documents/com~apple~CloudDocs");
|
||||
|
||||
if (Environment.CurrentDirectory.StartsWithIgnoreCase(icloudFiles))
|
||||
{
|
||||
Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run from the iCloud folder. Exiting...");
|
||||
macOSNativeInterop.SimpleMessageBox($"Ryujinx {Version}",
|
||||
"Ryujinx is not intended to be run from the iCloud folder.", "Ok");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsLinux())
|
||||
{
|
||||
if (root)
|
||||
{
|
||||
Logger.Error?.PrintMsg(LogClass.Application, "Ryujinx is not intended to be run as administrator. Exiting...");
|
||||
LinuxSDLInterop.SimpleMessageBox($"Ryujinx {Version}", "Ryujinx is not intended to be run as administrator.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
string container = Environment.GetEnvironmentVariable("container");
|
||||
|
||||
if (container is not null && container.EqualsIgnoreCase("flatpak"))
|
||||
{
|
||||
Logger.Info?.PrintMsg(LogClass.Application, "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=");
|
||||
Logger.Warning?.PrintMsg(LogClass.Application, "This is very likely an unofficial build, Ryujinx does NOT have a flatpak!");
|
||||
Logger.Info?.PrintMsg(LogClass.Application, "Please visit https://ryujinx.app/ for our official builds.");
|
||||
Logger.Info?.PrintMsg(LogClass.Application, "=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool noGuiArg = ConsumeCommandLineArgument(ref args, "--no-gui") || ConsumeCommandLineArgument(ref args, "nogui");
|
||||
bool coreDumpArg = ConsumeCommandLineArgument(ref args, "--core-dumps");
|
||||
@@ -115,6 +198,12 @@ namespace Ryujinx.Ava
|
||||
public static AppBuilder BuildAvaloniaApp() =>
|
||||
AppBuilder.Configure<RyujinxApp>()
|
||||
.UsePlatformDetect()
|
||||
|
||||
// Vulkan UI rendering performs better, but its unpolished, and as such it lacks effective transparency.
|
||||
// https://github.com/AvaloniaUI/Avalonia/issues/19378
|
||||
// https://github.com/AvaloniaUI/Avalonia/issues/9610
|
||||
// X11RenderingMode.Glx && X11RenderingMode.Egl, Win32RenderingMode.Vulkan have these issues.
|
||||
|
||||
.With(new X11PlatformOptions
|
||||
{
|
||||
EnableMultiTouch = true,
|
||||
@@ -310,7 +399,7 @@ namespace Ryujinx.Ava
|
||||
"never" => HideCursorMode.Never,
|
||||
"onidle" => HideCursorMode.OnIdle,
|
||||
"always" => HideCursorMode.Always,
|
||||
_ => ConfigurationState.Instance.HideCursor,
|
||||
_ => ConfigurationState.Instance.HideCursor
|
||||
};
|
||||
|
||||
// Check if memoryManagerMode was overridden.
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<PackageReference Include="OpenTK.Core" />
|
||||
<PackageReference Include="Ryujinx.Audio.OpenAL" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'osx-x64' AND '$(RuntimeIdentifier)' != 'osx-arm64'" />
|
||||
<PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies.AllArch" />
|
||||
<PackageReference Include="Ryujinx.Graphics.Vulkan.Dependencies.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'win-x64' AND '$(RuntimeIdentifier)' != 'win-arm64'" />
|
||||
<PackageReference Include="Ryujinx.Graphics.Vulkan.MoltenVK" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'linux-arm64' AND '$(RuntimeIdentifier)' != 'win-x64' AND '$(RuntimeIdentifier)' != 'win-arm64'" />
|
||||
<PackageReference Include="Ryujinx.UpdateClient" />
|
||||
<PackageReference Include="Ryujinx.Systems.Update.Common" />
|
||||
<PackageReference Include="securifybv.ShellLink" />
|
||||
|
||||
@@ -46,6 +46,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -476,10 +477,10 @@ namespace Ryujinx.Ava.Systems
|
||||
|
||||
TouchScreenManager.Initialize(Device);
|
||||
|
||||
_viewModel.IsGameRunning = true;
|
||||
|
||||
Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
_viewModel.IsGameRunning = true;
|
||||
_viewModel.IsPaused = false;
|
||||
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI);
|
||||
});
|
||||
|
||||
@@ -578,7 +579,14 @@ namespace Ryujinx.Ava.Systems
|
||||
public void Stop()
|
||||
{
|
||||
_isActive = false;
|
||||
_viewModel.IsPaused = false;
|
||||
_playTimer.Stop();
|
||||
|
||||
GCSettings.LatencyMode = GCLatencyMode.Interactive;
|
||||
if (ConfigurationState.Instance.System.GCLowLatency)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, "Garbage collector set to interactive mode.");
|
||||
}
|
||||
}
|
||||
|
||||
private void Exit()
|
||||
@@ -662,6 +670,12 @@ namespace Ryujinx.Ava.Systems
|
||||
|
||||
_chrono.Stop();
|
||||
_playTimer.Stop();
|
||||
|
||||
GCSettings.LatencyMode = GCLatencyMode.Interactive;
|
||||
if (ConfigurationState.Instance.System.GCLowLatency)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, "Garbage collector set to interactive mode.");
|
||||
}
|
||||
}
|
||||
|
||||
public void DisposeGpu()
|
||||
@@ -722,8 +736,8 @@ namespace Ryujinx.Ava.Systems
|
||||
if (userError is UserError.NoFirmware)
|
||||
{
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogFirmwareNoFirmwareInstalledMessage],
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallEmbeddedMessage, firmwareVersion.VersionString),
|
||||
LocaleManager.Instance[LocaleKeys.Dialog_Firmware_InstallerNotInstalledMessage],
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.Dialog_Firmware_InstallerEmbeddedMessage, firmwareVersion.VersionString),
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||
string.Empty);
|
||||
@@ -755,8 +769,8 @@ namespace Ryujinx.Ava.Systems
|
||||
_viewModel.RefreshFirmwareStatus();
|
||||
|
||||
await ContentDialogHelper.CreateInfoDialog(
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstalledMessage, firmwareVersion.VersionString),
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogFirmwareInstallEmbeddedSuccessMessage, firmwareVersion.VersionString),
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.Dialog_Firmware_InstallerInstalledMessage, firmwareVersion.VersionString),
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.Dialog_Firmware_InstallerEmbeddedSuccessMessage, firmwareVersion.VersionString),
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogOk],
|
||||
string.Empty,
|
||||
LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
|
||||
@@ -915,7 +929,14 @@ namespace Ryujinx.Ava.Systems
|
||||
ApplicationLibrary.LoadAndSaveMetaData(Device.Processes.ActiveApplication.ProgramIdText,
|
||||
appMetadata => appMetadata.UpdatePreGame()
|
||||
);
|
||||
|
||||
_playTimer.Start();
|
||||
|
||||
if (ConfigurationState.Instance.System.GCLowLatency)
|
||||
{
|
||||
GCSettings.LatencyMode = GCLatencyMode.LowLatency;
|
||||
Logger.Info?.Print(LogClass.Application, "Garbage collector set to low latency mode.");
|
||||
}
|
||||
}
|
||||
|
||||
internal void Resume()
|
||||
@@ -926,6 +947,12 @@ namespace Ryujinx.Ava.Systems
|
||||
_playTimer.Start();
|
||||
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI);
|
||||
Logger.Info?.Print(LogClass.Emulation, "Emulation was resumed.");
|
||||
|
||||
if (ConfigurationState.Instance.System.GCLowLatency)
|
||||
{
|
||||
GCSettings.LatencyMode = GCLatencyMode.LowLatency;
|
||||
Logger.Info?.Print(LogClass.Application, "Garbage collector set to low latency mode.");
|
||||
}
|
||||
}
|
||||
|
||||
internal void Pause()
|
||||
@@ -936,6 +963,12 @@ namespace Ryujinx.Ava.Systems
|
||||
_playTimer.Stop();
|
||||
_viewModel.Title = TitleHelper.ActiveApplicationTitle(Device?.Processes.ActiveApplication, Program.Version, !ConfigurationState.Instance.ShowOldUI, LocaleManager.Instance[LocaleKeys.Paused]);
|
||||
Logger.Info?.Print(LogClass.Emulation, "Emulation was paused.");
|
||||
|
||||
GCSettings.LatencyMode = GCLatencyMode.Interactive;
|
||||
if (ConfigurationState.Instance.System.GCLowLatency)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, "Garbage collector set to interactive mode.");
|
||||
}
|
||||
}
|
||||
|
||||
private void InitEmulatedSwitch()
|
||||
@@ -1271,6 +1304,11 @@ namespace Ryujinx.Ava.Systems
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_viewModel.IsActive)
|
||||
{
|
||||
_inputManager.KeyboardDriver.Clear();
|
||||
}
|
||||
|
||||
NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
|
||||
|
||||
if (_viewModel.IsActive)
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
/// <summary>
|
||||
/// The current version of the file format
|
||||
/// </summary>
|
||||
public const int CurrentVersion = 72;
|
||||
public const int CurrentVersion = 73;
|
||||
|
||||
/// <summary>
|
||||
/// Version of the configuration file format
|
||||
@@ -471,6 +471,11 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
/// </summary>
|
||||
public bool UseHypervisor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable or disable low-latency mode for garbage collection
|
||||
/// </summary>
|
||||
public bool GCLowLatency { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enables or disables the GDB stub
|
||||
/// </summary>
|
||||
|
||||
@@ -13,6 +13,8 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Key = Ryujinx.Common.Configuration.Hid.Key;
|
||||
using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||
using RyuLogger = Ryujinx.Common.Logging.Logger;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.Configuration
|
||||
@@ -112,6 +114,7 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
System.IgnoreControllerApplet.Value = cff.IgnoreApplet;
|
||||
System.SkipUserProfilesManager.Value = cff.SkipUserProfiles;
|
||||
System.UseHypervisor.Value = cff.UseHypervisor;
|
||||
System.GCLowLatency.Value = cff.GCLowLatency;
|
||||
|
||||
UI.GuiColumns.FavColumn.Value = shouldLoadFromFile ? cff.GuiColumns.FavColumn : UI.GuiColumns.FavColumn.Value;
|
||||
UI.GuiColumns.IconColumn.Value = shouldLoadFromFile ? cff.GuiColumns.IconColumn : UI.GuiColumns.IconColumn.Value;
|
||||
@@ -270,45 +273,45 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
Id = "0",
|
||||
PlayerIndex = PlayerIndex.Player1,
|
||||
ControllerType = ControllerType.ProController,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
||||
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
|
||||
{
|
||||
DpadUp = Key.Up,
|
||||
DpadDown = Key.Down,
|
||||
DpadLeft = Key.Left,
|
||||
DpadRight = Key.Right,
|
||||
ButtonMinus = Key.Minus,
|
||||
ButtonL = Key.E,
|
||||
ButtonZl = Key.Q,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
DpadUp = PhysicalKey.Up,
|
||||
DpadDown = PhysicalKey.Down,
|
||||
DpadLeft = PhysicalKey.Left,
|
||||
DpadRight = PhysicalKey.Right,
|
||||
ButtonMinus = PhysicalKey.Minus,
|
||||
ButtonL = PhysicalKey.E,
|
||||
ButtonZl = PhysicalKey.Q,
|
||||
ButtonSl = PhysicalKey.Unbound,
|
||||
ButtonSr = PhysicalKey.Unbound,
|
||||
},
|
||||
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||
{
|
||||
StickUp = Key.W,
|
||||
StickDown = Key.S,
|
||||
StickLeft = Key.A,
|
||||
StickRight = Key.D,
|
||||
StickButton = Key.F,
|
||||
StickUp = PhysicalKey.W,
|
||||
StickDown = PhysicalKey.S,
|
||||
StickLeft = PhysicalKey.A,
|
||||
StickRight = PhysicalKey.D,
|
||||
StickButton = PhysicalKey.F,
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
||||
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
|
||||
{
|
||||
ButtonA = Key.Z,
|
||||
ButtonB = Key.X,
|
||||
ButtonX = Key.C,
|
||||
ButtonY = Key.V,
|
||||
ButtonPlus = Key.Plus,
|
||||
ButtonR = Key.U,
|
||||
ButtonZr = Key.O,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
ButtonA = PhysicalKey.Z,
|
||||
ButtonB = PhysicalKey.X,
|
||||
ButtonX = PhysicalKey.C,
|
||||
ButtonY = PhysicalKey.V,
|
||||
ButtonPlus = PhysicalKey.Plus,
|
||||
ButtonR = PhysicalKey.U,
|
||||
ButtonZr = PhysicalKey.O,
|
||||
ButtonSl = PhysicalKey.Unbound,
|
||||
ButtonSr = PhysicalKey.Unbound,
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||
{
|
||||
StickUp = Key.I,
|
||||
StickDown = Key.K,
|
||||
StickLeft = Key.J,
|
||||
StickRight = Key.L,
|
||||
StickButton = Key.H,
|
||||
StickUp = PhysicalKey.I,
|
||||
StickDown = PhysicalKey.K,
|
||||
StickLeft = PhysicalKey.J,
|
||||
StickRight = PhysicalKey.L,
|
||||
StickButton = PhysicalKey.H,
|
||||
},
|
||||
}
|
||||
];
|
||||
@@ -333,6 +336,7 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
EnableRumble = false,
|
||||
StrongRumble = 1f,
|
||||
WeakRumble = 1f,
|
||||
UseHDRumble = true
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -534,7 +538,8 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
{
|
||||
if (cff.AudioBackend is AudioBackend.SDL2)
|
||||
cff.AudioBackend = AudioBackend.SDL3;
|
||||
})
|
||||
}),
|
||||
(72, static cff => cff.GCLowLatency = false)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -418,6 +418,11 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> UseHypervisor { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Enable or disable low-latency garbage collection
|
||||
/// </summary>
|
||||
public ReactiveObject<bool> GCLowLatency { get; private set; }
|
||||
|
||||
public SystemSection()
|
||||
{
|
||||
Language = new ReactiveObject<Language>();
|
||||
@@ -471,6 +476,8 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
AudioVolume.LogChangesToValue(nameof(AudioVolume));
|
||||
UseHypervisor = new ReactiveObject<bool>();
|
||||
UseHypervisor.LogChangesToValue(nameof(UseHypervisor));
|
||||
GCLowLatency = new ReactiveObject<bool>();
|
||||
GCLowLatency.LogChangesToValue(nameof(GCLowLatency));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ using Ryujinx.Graphics.Vulkan;
|
||||
using Ryujinx.HLE;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Key = Ryujinx.Common.Configuration.Hid.Key;
|
||||
using PhysicalKey = Ryujinx.Common.Configuration.Hid.PhysicalKey;
|
||||
|
||||
namespace Ryujinx.Ava.Systems.Configuration
|
||||
{
|
||||
@@ -87,6 +89,7 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
IgnoreApplet = System.IgnoreControllerApplet,
|
||||
SkipUserProfiles = System.SkipUserProfilesManager,
|
||||
UseHypervisor = System.UseHypervisor,
|
||||
GCLowLatency = System.GCLowLatency,
|
||||
GuiColumns = new GuiColumns
|
||||
{
|
||||
FavColumn = UI.GuiColumns.FavColumn,
|
||||
@@ -287,45 +290,45 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
Name = "Keyboard",
|
||||
PlayerIndex = PlayerIndex.Player1,
|
||||
ControllerType = ControllerType.ProController,
|
||||
LeftJoycon = new LeftJoyconCommonConfig<Key>
|
||||
LeftJoycon = new LeftJoyconCommonConfig<PhysicalKey>
|
||||
{
|
||||
DpadUp = Key.Up,
|
||||
DpadDown = Key.Down,
|
||||
DpadLeft = Key.Left,
|
||||
DpadRight = Key.Right,
|
||||
ButtonMinus = Key.Minus,
|
||||
ButtonL = Key.E,
|
||||
ButtonZl = Key.Q,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
DpadUp = PhysicalKey.Up,
|
||||
DpadDown = PhysicalKey.Down,
|
||||
DpadLeft = PhysicalKey.Left,
|
||||
DpadRight = PhysicalKey.Right,
|
||||
ButtonMinus = PhysicalKey.Minus,
|
||||
ButtonL = PhysicalKey.E,
|
||||
ButtonZl = PhysicalKey.Q,
|
||||
ButtonSl = PhysicalKey.Unbound,
|
||||
ButtonSr = PhysicalKey.Unbound,
|
||||
},
|
||||
LeftJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
LeftJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||
{
|
||||
StickUp = Key.W,
|
||||
StickDown = Key.S,
|
||||
StickLeft = Key.A,
|
||||
StickRight = Key.D,
|
||||
StickButton = Key.F,
|
||||
StickUp = PhysicalKey.W,
|
||||
StickDown = PhysicalKey.S,
|
||||
StickLeft = PhysicalKey.A,
|
||||
StickRight = PhysicalKey.D,
|
||||
StickButton = PhysicalKey.F,
|
||||
},
|
||||
RightJoycon = new RightJoyconCommonConfig<Key>
|
||||
RightJoycon = new RightJoyconCommonConfig<PhysicalKey>
|
||||
{
|
||||
ButtonA = Key.Z,
|
||||
ButtonB = Key.X,
|
||||
ButtonX = Key.C,
|
||||
ButtonY = Key.V,
|
||||
ButtonPlus = Key.Plus,
|
||||
ButtonR = Key.U,
|
||||
ButtonZr = Key.O,
|
||||
ButtonSl = Key.Unbound,
|
||||
ButtonSr = Key.Unbound,
|
||||
ButtonA = PhysicalKey.Z,
|
||||
ButtonB = PhysicalKey.X,
|
||||
ButtonX = PhysicalKey.C,
|
||||
ButtonY = PhysicalKey.V,
|
||||
ButtonPlus = PhysicalKey.Plus,
|
||||
ButtonR = PhysicalKey.U,
|
||||
ButtonZr = PhysicalKey.O,
|
||||
ButtonSl = PhysicalKey.Unbound,
|
||||
ButtonSr = PhysicalKey.Unbound,
|
||||
},
|
||||
RightJoyconStick = new JoyconConfigKeyboardStick<Key>
|
||||
RightJoyconStick = new JoyconConfigKeyboardStick<PhysicalKey>
|
||||
{
|
||||
StickUp = Key.I,
|
||||
StickDown = Key.K,
|
||||
StickLeft = Key.J,
|
||||
StickRight = Key.L,
|
||||
StickButton = Key.H,
|
||||
StickUp = PhysicalKey.I,
|
||||
StickDown = PhysicalKey.K,
|
||||
StickLeft = PhysicalKey.J,
|
||||
StickRight = PhysicalKey.L,
|
||||
StickButton = PhysicalKey.H,
|
||||
},
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1070,6 +1070,23 @@ namespace Ryujinx.Ava.Systems.PlayReport
|
||||
_ => FormattedValue.ForceReset
|
||||
};
|
||||
|
||||
private static FormattedValue TomodachiLifeLTD_Status(SingleValue value)
|
||||
{
|
||||
MessagePackObject messagePackObject = value.Matched.PackedValue;
|
||||
MessagePackObjectDictionary messagePackObjectDictionary = messagePackObject.AsDictionary();
|
||||
|
||||
int miiCount = messagePackObjectDictionary["MiiNum"].AsInt32();
|
||||
int fountainLevel = messagePackObjectDictionary["FountainLevel"].AsInt32();
|
||||
|
||||
return $"Looking after {"Mii".ToQuantity(miiCount)}, with an island level of {fountainLevel}";
|
||||
}
|
||||
|
||||
private static FormattedValue AnimalCrossingNewHorizons_AppCommon(SingleValue value)
|
||||
{
|
||||
MessagePackObject messagePackObject = value.Matched.PackedValue;
|
||||
MessagePackObjectDictionary messagePackObjectDictionary = messagePackObject.AsDictionary();
|
||||
|
||||
return $"Living on {messagePackObjectDictionary["LandName"].AsString()} Island";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,19 @@ namespace Ryujinx.Ava.Systems.PlayReport
|
||||
"based on what game you first launch.\n\nNSO emulators do not print any Play Report information past the first game launch so it's all we got.")
|
||||
.AddValueFormatter("launch_title_id", NsoEmulator_LaunchedGame)
|
||||
)
|
||||
.AddSpec(
|
||||
[ "010051f0207b2000", "0100ca502552a000" ], // Tomodachi Life: Living the Dream + Demo
|
||||
spec => spec
|
||||
.WithDescription(
|
||||
"based on your total Mii count and island level.")
|
||||
.AddValueFormatter("Common", TomodachiLifeLTD_Status)
|
||||
)
|
||||
.AddSpec(
|
||||
"01006f8002326000", // Animal Crossing New Horizons
|
||||
spec => spec
|
||||
.WithDescription("based on your island name.")
|
||||
.AddValueFormatter("AppCmn", AnimalCrossingNewHorizons_AppCommon)
|
||||
)
|
||||
);
|
||||
|
||||
private static string Playing(string game) => $"Playing {game}";
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace Ryujinx.Ava.UI.Applet
|
||||
class AvaloniaDynamicTextInputHandler : IDynamicTextInputHandler
|
||||
{
|
||||
private MainWindow _parent;
|
||||
private AvaloniaKeyboardDriver _avaloniaKeyboardDriver;
|
||||
private readonly OffscreenTextBox _hiddenTextBox;
|
||||
private bool _canProcessInput;
|
||||
private IDisposable _textChangedSubscription;
|
||||
@@ -27,6 +28,7 @@ namespace Ryujinx.Ava.UI.Applet
|
||||
|
||||
if (_parent.InputManager.KeyboardDriver is AvaloniaKeyboardDriver avaloniaKeyboardDriver)
|
||||
{
|
||||
_avaloniaKeyboardDriver = avaloniaKeyboardDriver;
|
||||
avaloniaKeyboardDriver.KeyPressed += AvaloniaDynamicTextInputHandler_KeyPressed;
|
||||
avaloniaKeyboardDriver.KeyRelease += AvaloniaDynamicTextInputHandler_KeyRelease;
|
||||
avaloniaKeyboardDriver.TextInput += AvaloniaDynamicTextInputHandler_TextInput;
|
||||
@@ -65,7 +67,7 @@ namespace Ryujinx.Ava.UI.Applet
|
||||
|
||||
private void AvaloniaDynamicTextInputHandler_KeyRelease(object sender, KeyEventArgs e)
|
||||
{
|
||||
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.Key);
|
||||
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.PhysicalKey, e.Key);
|
||||
|
||||
if (!(KeyReleasedEvent?.Invoke(key)).GetValueOrDefault(true))
|
||||
{
|
||||
@@ -85,7 +87,7 @@ namespace Ryujinx.Ava.UI.Applet
|
||||
|
||||
private void AvaloniaDynamicTextInputHandler_KeyPressed(object sender, KeyEventArgs e)
|
||||
{
|
||||
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.Key);
|
||||
HidKey key = (HidKey)AvaloniaKeyboardMappingHelper.ToInputKey(e.PhysicalKey, e.Key);
|
||||
|
||||
if (!(KeyPressedEvent?.Invoke(key)).GetValueOrDefault(true))
|
||||
{
|
||||
@@ -115,11 +117,11 @@ namespace Ryujinx.Ava.UI.Applet
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_parent.InputManager.KeyboardDriver is AvaloniaKeyboardDriver avaloniaKeyboardDriver)
|
||||
if (_avaloniaKeyboardDriver != null)
|
||||
{
|
||||
avaloniaKeyboardDriver.KeyPressed -= AvaloniaDynamicTextInputHandler_KeyPressed;
|
||||
avaloniaKeyboardDriver.KeyRelease -= AvaloniaDynamicTextInputHandler_KeyRelease;
|
||||
avaloniaKeyboardDriver.TextInput -= AvaloniaDynamicTextInputHandler_TextInput;
|
||||
_avaloniaKeyboardDriver.KeyPressed -= AvaloniaDynamicTextInputHandler_KeyPressed;
|
||||
_avaloniaKeyboardDriver.KeyRelease -= AvaloniaDynamicTextInputHandler_KeyRelease;
|
||||
_avaloniaKeyboardDriver.TextInput -= AvaloniaDynamicTextInputHandler_TextInput;
|
||||
}
|
||||
|
||||
_textChangedSubscription?.Dispose();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Threading;
|
||||
using Ryujinx.Ava.Input;
|
||||
using Ryujinx.Input;
|
||||
using Ryujinx.Input.Assigner;
|
||||
using System;
|
||||
@@ -25,6 +26,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
|
||||
private bool _isWaitingForInput;
|
||||
private bool _shouldUnbind;
|
||||
private IKeyboard _keyboard;
|
||||
public event EventHandler<ButtonAssignedEventArgs> ButtonAssigned;
|
||||
|
||||
public ButtonKeyAssigner(ToggleButton toggleButton)
|
||||
@@ -34,6 +36,9 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
|
||||
public async void GetInputAndAssign(IButtonAssigner assigner, IKeyboard keyboard = null)
|
||||
{
|
||||
_keyboard = keyboard;
|
||||
ClearKeyboardState(_keyboard);
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
ToggledButton.IsChecked = true;
|
||||
@@ -82,6 +87,7 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
_isWaitingForInput = false;
|
||||
|
||||
ToggledButton.IsChecked = false;
|
||||
ClearKeyboardState(_keyboard);
|
||||
|
||||
if (pressedButton.HasValue && pressedButton.Value.AsHidType<Key>() == Key.BackSpace)
|
||||
{
|
||||
@@ -98,6 +104,15 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
_isWaitingForInput = false;
|
||||
ToggledButton.IsChecked = false;
|
||||
_shouldUnbind = shouldUnbind;
|
||||
ClearKeyboardState(_keyboard);
|
||||
}
|
||||
|
||||
private static void ClearKeyboardState(IKeyboard keyboard)
|
||||
{
|
||||
if (keyboard is AvaloniaKeyboard avaloniaKeyboard)
|
||||
{
|
||||
avaloniaKeyboard.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Ryujinx.Common.Configuration.Hid.Controller;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Key = Ryujinx.Input.Key;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
@@ -12,79 +13,6 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
{
|
||||
public static readonly KeyValueConverter Instance = new();
|
||||
|
||||
private static readonly Dictionary<Key, LocaleKeys> _keysMap = new()
|
||||
{
|
||||
{ Key.Unknown, LocaleKeys.KeyUnknown },
|
||||
{ Key.ShiftLeft, LocaleKeys.KeyShiftLeft },
|
||||
{ Key.ShiftRight, LocaleKeys.KeyShiftRight },
|
||||
{ Key.ControlLeft, LocaleKeys.KeyControlLeft },
|
||||
{ Key.ControlRight, LocaleKeys.KeyControlRight },
|
||||
{ Key.AltLeft, LocaleKeys.KeyAltLeft },
|
||||
{ Key.AltRight, LocaleKeys.KeyAltRight },
|
||||
{ Key.WinLeft, LocaleKeys.KeyWinLeft },
|
||||
{ Key.WinRight, LocaleKeys.KeyWinRight },
|
||||
{ Key.Up, LocaleKeys.KeyUp },
|
||||
{ Key.Down, LocaleKeys.KeyDown },
|
||||
{ Key.Left, LocaleKeys.KeyLeft },
|
||||
{ Key.Right, LocaleKeys.KeyRight },
|
||||
{ Key.Enter, LocaleKeys.KeyEnter },
|
||||
{ Key.Escape, LocaleKeys.KeyEscape },
|
||||
{ Key.Space, LocaleKeys.KeySpace },
|
||||
{ Key.Tab, LocaleKeys.KeyTab },
|
||||
{ Key.BackSpace, LocaleKeys.KeyBackSpace },
|
||||
{ Key.Insert, LocaleKeys.KeyInsert },
|
||||
{ Key.Delete, LocaleKeys.KeyDelete },
|
||||
{ Key.PageUp, LocaleKeys.KeyPageUp },
|
||||
{ Key.PageDown, LocaleKeys.KeyPageDown },
|
||||
{ Key.Home, LocaleKeys.KeyHome },
|
||||
{ Key.End, LocaleKeys.KeyEnd },
|
||||
{ Key.CapsLock, LocaleKeys.KeyCapsLock },
|
||||
{ Key.ScrollLock, LocaleKeys.KeyScrollLock },
|
||||
{ Key.PrintScreen, LocaleKeys.KeyPrintScreen },
|
||||
{ Key.Pause, LocaleKeys.KeyPause },
|
||||
{ Key.NumLock, LocaleKeys.KeyNumLock },
|
||||
{ Key.Clear, LocaleKeys.KeyClear },
|
||||
{ Key.Keypad0, LocaleKeys.KeyKeypad0 },
|
||||
{ Key.Keypad1, LocaleKeys.KeyKeypad1 },
|
||||
{ Key.Keypad2, LocaleKeys.KeyKeypad2 },
|
||||
{ Key.Keypad3, LocaleKeys.KeyKeypad3 },
|
||||
{ Key.Keypad4, LocaleKeys.KeyKeypad4 },
|
||||
{ Key.Keypad5, LocaleKeys.KeyKeypad5 },
|
||||
{ Key.Keypad6, LocaleKeys.KeyKeypad6 },
|
||||
{ Key.Keypad7, LocaleKeys.KeyKeypad7 },
|
||||
{ Key.Keypad8, LocaleKeys.KeyKeypad8 },
|
||||
{ Key.Keypad9, LocaleKeys.KeyKeypad9 },
|
||||
{ Key.KeypadDivide, LocaleKeys.KeyKeypadDivide },
|
||||
{ Key.KeypadMultiply, LocaleKeys.KeyKeypadMultiply },
|
||||
{ Key.KeypadSubtract, LocaleKeys.KeyKeypadSubtract },
|
||||
{ Key.KeypadAdd, LocaleKeys.KeyKeypadAdd },
|
||||
{ Key.KeypadDecimal, LocaleKeys.KeyKeypadDecimal },
|
||||
{ Key.KeypadEnter, LocaleKeys.KeyKeypadEnter },
|
||||
{ Key.Number0, LocaleKeys.KeyNumber0 },
|
||||
{ Key.Number1, LocaleKeys.KeyNumber1 },
|
||||
{ Key.Number2, LocaleKeys.KeyNumber2 },
|
||||
{ Key.Number3, LocaleKeys.KeyNumber3 },
|
||||
{ Key.Number4, LocaleKeys.KeyNumber4 },
|
||||
{ Key.Number5, LocaleKeys.KeyNumber5 },
|
||||
{ Key.Number6, LocaleKeys.KeyNumber6 },
|
||||
{ Key.Number7, LocaleKeys.KeyNumber7 },
|
||||
{ Key.Number8, LocaleKeys.KeyNumber8 },
|
||||
{ Key.Number9, LocaleKeys.KeyNumber9 },
|
||||
{ Key.Tilde, LocaleKeys.KeyTilde },
|
||||
{ Key.Grave, LocaleKeys.KeyGrave },
|
||||
{ Key.Minus, LocaleKeys.KeyMinus },
|
||||
{ Key.Plus, LocaleKeys.KeyPlus },
|
||||
{ Key.BracketLeft, LocaleKeys.KeyBracketLeft },
|
||||
{ Key.BracketRight, LocaleKeys.KeyBracketRight },
|
||||
{ Key.Semicolon, LocaleKeys.KeySemicolon },
|
||||
{ Key.Quote, LocaleKeys.KeyQuote },
|
||||
{ Key.Comma, LocaleKeys.KeyComma },
|
||||
{ Key.Period, LocaleKeys.KeyPeriod },
|
||||
{ Key.Slash, LocaleKeys.KeySlash },
|
||||
{ Key.BackSlash, LocaleKeys.KeyBackSlash },
|
||||
{ Key.Unbound, LocaleKeys.KeyUnbound },
|
||||
};
|
||||
|
||||
private static readonly Dictionary<GamepadInputId, LocaleKeys> _gamepadInputIdMap = new()
|
||||
{
|
||||
{ GamepadInputId.LeftStick, LocaleKeys.GamepadLeftStick },
|
||||
@@ -110,78 +38,40 @@ namespace Ryujinx.Ava.UI.Helpers
|
||||
{ GamepadInputId.SingleRightTrigger0, LocaleKeys.GamepadSingleRightTrigger0},
|
||||
{ GamepadInputId.SingleLeftTrigger1, LocaleKeys.GamepadSingleLeftTrigger1},
|
||||
{ GamepadInputId.SingleRightTrigger1, LocaleKeys.GamepadSingleRightTrigger1},
|
||||
{ GamepadInputId.Unbound, LocaleKeys.KeyUnbound},
|
||||
{ GamepadInputId.Unbound, LocaleKeys.KeyboardLayout_KeyUnbound},
|
||||
};
|
||||
|
||||
private static readonly Dictionary<StickInputId, LocaleKeys> _stickInputIdMap = new()
|
||||
{
|
||||
{ StickInputId.Left, LocaleKeys.StickLeft},
|
||||
{ StickInputId.Right, LocaleKeys.StickRight},
|
||||
{ StickInputId.Unbound, LocaleKeys.KeyUnbound},
|
||||
{ StickInputId.Unbound, LocaleKeys.KeyboardLayout_KeyUnbound},
|
||||
};
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
string keyString = string.Empty;
|
||||
LocaleKeys localeKey;
|
||||
|
||||
switch (value)
|
||||
return value switch
|
||||
{
|
||||
case Key key:
|
||||
if (_keysMap.TryGetValue(key, out localeKey))
|
||||
{
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
localeKey = localeKey switch
|
||||
{
|
||||
LocaleKeys.KeyControlLeft => LocaleKeys.KeyMacControlLeft,
|
||||
LocaleKeys.KeyControlRight => LocaleKeys.KeyMacControlRight,
|
||||
LocaleKeys.KeyAltLeft => LocaleKeys.KeyMacAltLeft,
|
||||
LocaleKeys.KeyAltRight => LocaleKeys.KeyMacAltRight,
|
||||
LocaleKeys.KeyWinLeft => LocaleKeys.KeyMacWinLeft,
|
||||
LocaleKeys.KeyWinRight => LocaleKeys.KeyMacWinRight,
|
||||
_ => localeKey
|
||||
};
|
||||
}
|
||||
|
||||
keyString = LocaleManager.Instance[localeKey];
|
||||
}
|
||||
else
|
||||
{
|
||||
keyString = key.ToString();
|
||||
}
|
||||
|
||||
break;
|
||||
case GamepadInputId gamepadInputId:
|
||||
if (_gamepadInputIdMap.TryGetValue(gamepadInputId, out localeKey))
|
||||
{
|
||||
keyString = LocaleManager.Instance[localeKey];
|
||||
}
|
||||
else
|
||||
{
|
||||
keyString = gamepadInputId.ToString();
|
||||
}
|
||||
|
||||
break;
|
||||
case StickInputId stickInputId:
|
||||
if (_stickInputIdMap.TryGetValue(stickInputId, out localeKey))
|
||||
{
|
||||
keyString = LocaleManager.Instance[localeKey];
|
||||
}
|
||||
else
|
||||
{
|
||||
keyString = stickInputId.ToString();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return keyString;
|
||||
Key key => KeyboardLayoutLocaleHelper.TryGetSemanticLabel(key, out string localizedKeyLabel)
|
||||
? localizedKeyLabel
|
||||
: key.ToString(),
|
||||
PhysicalKey physicalKey => PhysicalKeyLabelHelper.GetDisplayString(physicalKey),
|
||||
GamepadInputId gamepadInputId => GetLocalizedMappedValue(gamepadInputId, _gamepadInputIdMap),
|
||||
StickInputId stickInputId => GetLocalizedMappedValue(stickInputId, _stickInputIdMap),
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
|
||||
private static string GetLocalizedMappedValue<T>(T value, IReadOnlyDictionary<T, LocaleKeys> map) where T : notnull
|
||||
{
|
||||
return map.TryGetValue(value, out LocaleKeys localeKey)
|
||||
? LocaleManager.Instance[localeKey]
|
||||
: value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user