mirror of
https://git.ryujinx.app/ryubing/ryujinx.git
synced 2026-06-12 07:19:14 +00:00
Compare commits
135 Commits
Canary-1.3
...
719d879720
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
719d879720 | ||
|
|
d1205dc95d | ||
|
|
6f95172bb6 | ||
|
|
8208d43d9e | ||
|
|
ecd503cf20 | ||
|
|
1260f93aaf | ||
|
|
36d9466427 | ||
|
|
1b3bf1473d | ||
|
|
081cdcab0c | ||
|
|
922775664c | ||
|
|
6282db2cb0 | ||
|
|
478b66fd49 | ||
|
|
6ea4ce3404 | ||
|
|
a16a072155 | ||
|
|
72c88b9b50 | ||
|
|
a4a0fcd4da | ||
|
|
cc5b60bbca | ||
|
|
5ed94c365b | ||
|
|
fef93a453a | ||
|
|
82074eb191 | ||
|
|
bd388cf4f9 | ||
|
|
d271abe19a | ||
|
|
0baf3b84ed | ||
|
|
c154f66f26 | ||
|
|
1552d31e01 | ||
|
|
f556e8b8fb | ||
|
|
5d0fc6d456 | ||
|
|
5993c78344 | ||
|
|
e1b01f2e70 | ||
|
|
e3bdda7afa | ||
|
|
0d5503c014 | ||
|
|
b26375cc4b | ||
|
|
85dad79581 | ||
|
|
2941951f4d | ||
|
|
925ae1652b | ||
|
|
af59454e19 | ||
|
|
beb7dfe7a6 | ||
|
|
99feaafbe6 | ||
|
|
85e62c3ad2 | ||
|
|
fa55608587 | ||
|
|
4c64300576 | ||
|
|
0a3db19b28 | ||
|
|
453b246faa | ||
|
|
082a2a2051 | ||
|
|
2b2159f330 | ||
|
|
3620c76cc1 | ||
|
|
5a8f6fa46d | ||
|
|
45193dcc8d | ||
|
|
813057acbf | ||
|
|
620eba5fcb | ||
|
|
829a5561ab | ||
|
|
9ebf444644 | ||
|
|
0aec32f437 | ||
|
|
6619de59ab | ||
|
|
746dc2cd22 | ||
|
|
17ab09119e | ||
|
|
46bae1c40a | ||
|
|
5536bba1fa | ||
|
|
8c6b642129 | ||
|
|
76dd747811 | ||
|
|
1c073ebc63 | ||
|
|
cebe423df2 | ||
|
|
93f53b24b8 | ||
|
|
cb5c9ce585 | ||
|
|
167b41354b | ||
|
|
e52fa0b9d1 | ||
|
|
6ed92dd9b7 | ||
|
|
d9846faa5f | ||
|
|
928a189d99 | ||
|
|
de11115971 | ||
|
|
c093b34767 | ||
|
|
13036dcd5b | ||
|
|
51bec1f4a2 | ||
|
|
00cb9e42f8 | ||
|
|
5be56d0ccf | ||
|
|
a9bb932491 | ||
|
|
e1f215de46 | ||
|
|
b6eb78598c | ||
|
|
1392fcfbc5 | ||
|
|
9b82e8452f | ||
|
|
379ce9e7aa | ||
|
|
fc89c17037 | ||
|
|
222db1a736 | ||
|
|
89c6c490a3 | ||
|
|
4d3a98e71d | ||
|
|
18233cf7e6 | ||
|
|
c0cc54cc56 | ||
|
|
d9ab68b1e9 | ||
|
|
e7e0d4d877 | ||
|
|
a2fa346cfd | ||
|
|
6ae279300c | ||
|
|
82e392604d | ||
|
|
860112c910 | ||
|
|
0ecef83316 | ||
|
|
93256afd24 | ||
|
|
5d3f22ac57 | ||
|
|
5b63aabe8b | ||
|
|
9b6dfab66e | ||
|
|
9e1ee169d9 | ||
|
|
0958796a29 | ||
|
|
f257481cdb | ||
|
|
6533270499 | ||
|
|
c3c6f36fea | ||
|
|
df153efadf | ||
|
|
4b2362f18b | ||
|
|
c2fc1a8582 | ||
|
|
598d6076ee | ||
|
|
69e2ea2894 | ||
|
|
93fe2d36aa | ||
|
|
0d2f280303 | ||
|
|
24ac55f4d6 | ||
|
|
af5d9a90b7 | ||
|
|
d52415b535 | ||
|
|
4d56f4dcd3 | ||
|
|
57c91089f7 | ||
|
|
15c9d50815 | ||
|
|
4012fecc25 | ||
|
|
80df6e2336 | ||
|
|
9e2837d885 | ||
|
|
96028daff1 | ||
|
|
46fa8c1426 | ||
|
|
35aacdb289 | ||
|
|
7a2802d870 | ||
|
|
8548d35620 | ||
|
|
07ef8e9c9a | ||
|
|
5963b425d1 | ||
|
|
7f5a67434e | ||
|
|
3abfeebd58 | ||
|
|
8b438c69db | ||
|
|
1d86653c9d | ||
|
|
79f3ea5cfa | ||
|
|
ba656e560b | ||
|
|
5a6d476490 | ||
|
|
16c35344d8 | ||
|
|
9f2ab7aa8f |
18
Ryujinx.sln
18
Ryujinx.sln
@@ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.GAL", "src
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.OpenGL", "src\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj", "{9558FB96-075D-4219-8FFF-401979DC0B69}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Graphics.RenderDoc", "src\Ryujinx.Graphics.RenderDocApi\Ryujinx.Graphics.RenderDocApi.csproj", "{D58FA894-27D5-4EAA-9042-AD422AD82931}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Texture", "src\Ryujinx.Graphics.Texture\Ryujinx.Graphics.Texture.csproj", "{E1B1AD28-289D-47B7-A106-326972240207}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Shader", "src\Ryujinx.Graphics.Shader\Ryujinx.Graphics.Shader.csproj", "{03B955CD-AD84-4B93-AAA7-BF17923BBAA5}"
|
||||
@@ -45,6 +47,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Vic", "src
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Graphics.Video", "src\Ryujinx.Graphics.Video\Ryujinx.Graphics.Video.csproj", "{FD4A2C14-8E3D-4957-ABBE-3C38897B3E2D}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.Apple", "src\Ryujinx.Audio.Backends.Apple\Ryujinx.Audio.Backends.Apple.csproj", "{AC26EFF0-8593-4184-9A09-98E37EFFB32E}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.SDL3", "src\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj", "{988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Audio.Backends.OpenAL", "src\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj", "{0BE11899-DF2D-4BDE-B9EE-2489E8D35E7D}"
|
||||
@@ -555,6 +559,20 @@ Global
|
||||
{988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x64.Build.0 = Release|Any CPU
|
||||
{988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{988E6191-82E1-4E13-9DDB-CB9FA2FDAF29}.Release|x86.Build.0 = Release|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D58FA894-27D5-4EAA-9042-AD422AD82931}.Release|x86.Build.0 = Release|Any CPU
|
||||
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AC26EFF0-8593-4184-9A09-98E37EFFB32E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
24
assets/Languages.json
Normal file
24
assets/Languages.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"Languages": {
|
||||
"ar_SA": "اَلْعَرَبِيَّةُ",
|
||||
"de_DE": "Deutsch",
|
||||
"el_GR": "Ελληνικά",
|
||||
"en_US": "English (US)",
|
||||
"es_ES": "Español (ES)",
|
||||
"fr_FR": "Français (FR)",
|
||||
"he_IL": "עִברִית",
|
||||
"it_IT": "Italiano",
|
||||
"ja_JP": "日本語",
|
||||
"ko_KR": "한국어",
|
||||
"no_NO": "Norsk",
|
||||
"pl_PL": "Polski",
|
||||
"pt_BR": "Português (BR)",
|
||||
"ru_RU": "Русский",
|
||||
"sv_SE": "Svenska",
|
||||
"th_TH": "ภาษาไทย",
|
||||
"tr_TR": "Türkçe",
|
||||
"uk_UA": "Українська",
|
||||
"zh_CN": "简体中文",
|
||||
"zh_TW": "繁體中文 (台灣)"
|
||||
}
|
||||
}
|
||||
60
assets/Locales.md
Normal file
60
assets/Locales.md
Normal file
@@ -0,0 +1,60 @@
|
||||
|
||||
# Ryubing Locales
|
||||
|
||||
Ryubing Locales uses a custom format, which uses a file for defining the supported languages and a folder of json files for the locales themselves.
|
||||
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.
|
||||
|
||||
#Example of the format for Languages.json
|
||||
{
|
||||
"Languages": {
|
||||
"ar_SA": "اَلْعَرَبِيَّةُ",
|
||||
"en_US": "English (US)",
|
||||
...
|
||||
"zh_TW": "繁體中文 (台灣)"
|
||||
}
|
||||
}
|
||||
|
||||
## Locales
|
||||
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.
|
||||
|
||||
### Format
|
||||
When adding a new locale, you just need to add the ID and the en_US language translation, then the validation system will add default values for the rest of languages automatically, when rebuilding the project.
|
||||
If you want to signal that a translation is supposed to match the English translation, you just have to replace the empty string with `null`.
|
||||
When you want to check what translations are missing for a language just search for `"<lang_code>": ""`, e.g: `"en_US": ""` (but with any other language, as English will never be missing translations).
|
||||
|
||||
### Legacy file (Root.json)
|
||||
Currently all older locales are stored in `Root.json`, but they are slowly being moved into newer, more descriptive json files, to make the locale system more accessible.
|
||||
Do **not** add new locales to `Root.json`.
|
||||
If no json file exists for the specific part of the emulator you're working on, you should instead add a new json file for that part.
|
||||
|
||||
#Example of the format for Root.json
|
||||
{
|
||||
"Locales": [
|
||||
{
|
||||
"ID": "MenuBarActionsOpenMiiEditor",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"en_US": "Mii Editor",
|
||||
...
|
||||
"zh_TW": "Mii 編輯器"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "KeyNumber9",
|
||||
"Translations": {
|
||||
"ar_SA": "٩",
|
||||
"en_US": "9",
|
||||
...
|
||||
"zh_TW": null
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
104
assets/Locales/RenderDoc.json
Normal file
104
assets/Locales/RenderDoc.json
Normal file
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"Locales": [
|
||||
{
|
||||
"ID": "MenuBarActions_StartCapture",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"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": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "MenuBarActions_EndCapture",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"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": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "MenuBarActions_DiscardCapture",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"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": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "MenuBarActions_DiscardCapture_ToolTip",
|
||||
"Translations": {
|
||||
"ar_SA": "",
|
||||
"de_DE": "",
|
||||
"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": ""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
854
assets/Locales/UserProfiles.json
Normal file
854
assets/Locales/UserProfiles.json
Normal file
@@ -0,0 +1,854 @@
|
||||
{
|
||||
"Locales": [
|
||||
{
|
||||
"ID": "MenuBarOptions_OpenUserProfiles",
|
||||
"Translations": {
|
||||
"ar_SA": "_ملفات المستخدمين",
|
||||
"de_DE": "_Benutzerprofile",
|
||||
"el_GR": "_Προφίλ Χρηστών",
|
||||
"en_US": "_User Profiles",
|
||||
"es_ES": "_Perfiles de Usuario",
|
||||
"fr_FR": "_Profils d'Utilisateurs",
|
||||
"he_IL": "_פרופילי משתמש",
|
||||
"it_IT": "_Profili utent",
|
||||
"ja_JP": "ユーザプロファイル(_M)",
|
||||
"ko_KR": "사용자 프로필(_M)",
|
||||
"no_NO": "_Brukerprofiler",
|
||||
"pl_PL": "_Profile użytkowników",
|
||||
"pt_BR": "_Perfis de usuário",
|
||||
"ru_RU": "_Учётные записи",
|
||||
"sv_SE": "_Användarprofiler",
|
||||
"th_TH": "_โปรไฟล์ผู้ใช้งาน",
|
||||
"tr_TR": "_Kullanıcı Profilleri",
|
||||
"uk_UA": "_Профілі користувачів",
|
||||
"zh_CN": "用户配置文件(_M)",
|
||||
"zh_TW": "使用者設定檔(_M)"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "WindowTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "ملفات المستخدمين",
|
||||
"de_DE": "Benutzerprofile",
|
||||
"el_GR": "Προφίλ Χρηστών",
|
||||
"en_US": "User Profiles",
|
||||
"es_ES": "Perfiles de Usuario",
|
||||
"fr_FR": "Profils d'Utilisateurs",
|
||||
"he_IL": "פרופילי משתמש",
|
||||
"it_IT": "Profili utent",
|
||||
"ja_JP": "ユーザプロファイル",
|
||||
"ko_KR": "사용자 프로필",
|
||||
"no_NO": "Brukerprofiler",
|
||||
"pl_PL": "Profile użytkowników",
|
||||
"pt_BR": "Perfis de usuário",
|
||||
"ru_RU": "Учётные записи",
|
||||
"sv_SE": "Användarprofiler",
|
||||
"th_TH": "โปรไฟล์ผู้ใช้งาน",
|
||||
"tr_TR": "Kullanıcı Profilleri",
|
||||
"uk_UA": "Профілі користувачів",
|
||||
"zh_CN": "用户配置文件",
|
||||
"zh_TW": "使用者設定檔"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ManageSaves",
|
||||
"Translations": {
|
||||
"ar_SA": "عمليات الحفظ",
|
||||
"de_DE": "Speicherstände",
|
||||
"el_GR": "Αποθηκεύσεις",
|
||||
"en_US": "Saves",
|
||||
"es_ES": "Partidas",
|
||||
"fr_FR": "Sauvegardes",
|
||||
"he_IL": "שמירות",
|
||||
"it_IT": "Salvataggi",
|
||||
"ja_JP": "セーブデータ",
|
||||
"ko_KR": "저장",
|
||||
"no_NO": "Lagringer",
|
||||
"pl_PL": "Zapisy",
|
||||
"pt_BR": "Salvamentos",
|
||||
"ru_RU": "Сохранения",
|
||||
"sv_SE": "Sparningar",
|
||||
"th_TH": "บันทึก",
|
||||
"tr_TR": "Kayıtlar",
|
||||
"uk_UA": "Збереження",
|
||||
"zh_CN": "存档",
|
||||
"zh_TW": "存檔"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DeleteSaveNote",
|
||||
"Translations": {
|
||||
"ar_SA": "هل حذف بيانات حفظ المستخدم لهذه اللعبة؟",
|
||||
"de_DE": "Löschen Sie die gespeicherten Spielstände dieses Spiels?",
|
||||
"el_GR": "Διαγραφή των δεδομένων αποθήκευσης αυτού του παιχνιδιού;",
|
||||
"en_US": "Delete this game's save data?",
|
||||
"es_ES": "¿Eliminar los datos de guardado de este juego?",
|
||||
"fr_FR": "Supprimer les données de sauvegarde de ce jeu ?",
|
||||
"he_IL": "האם למחוק את נתוני השמירה של המשחק הזה?",
|
||||
"it_IT": "Eliminare i dati di salvataggio di questo gioco?",
|
||||
"ja_JP": "このゲームのセーブデータを削除しますか?",
|
||||
"ko_KR": "이 게임의 저장 데이터를 삭제하시겠습니까?",
|
||||
"no_NO": "Slette lagrede data for dette spillet?",
|
||||
"pl_PL": "Usunąć dane zapisu dla tej gry?",
|
||||
"pt_BR": "Excluir os dados salvos deste jogo?",
|
||||
"ru_RU": "Удалить данные сохранений для этой игры?",
|
||||
"sv_SE": "Ta bort sparad data för detta spel?",
|
||||
"th_TH": "ลบข้อมูลบันทึกของเกมนี้หรือไม่?",
|
||||
"tr_TR": "Bu oyun için kaydedilen veriyi silmek?",
|
||||
"uk_UA": "Видалити збереження даних для цієї гри?",
|
||||
"zh_CN": "删除此游戏的存档数据?",
|
||||
"zh_TW": "刪除此遊戲的存檔資料?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SaveManagerTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "حفظات {0}",
|
||||
"de_DE": "{0}s Speicherstände",
|
||||
"el_GR": "Αποθηκεύσεις του {0}",
|
||||
"en_US": "{0}'s Saves",
|
||||
"es_ES": "Guardados de {0}",
|
||||
"fr_FR": "Sauvegardes de {0}",
|
||||
"he_IL": "שמירות של {0}",
|
||||
"it_IT": "Salvataggi di {0}",
|
||||
"ja_JP": "{0} のセーブデータ",
|
||||
"ko_KR": "{0} 의 저장",
|
||||
"no_NO": "Lagringer til {0}",
|
||||
"pl_PL": "Zapisy {0}",
|
||||
"pt_BR": "Salvamentos de {0}",
|
||||
"ru_RU": "Сохранения {0}",
|
||||
"sv_SE": "{0}s Sparningar",
|
||||
"th_TH": "ข้อมูลที่บันทึกไว้ของ {0}",
|
||||
"tr_TR": "{0}’nin Kayıtları",
|
||||
"uk_UA": "Збереження {0}",
|
||||
"zh_CN": "{0} 的存档",
|
||||
"zh_TW": "{0} 的存檔"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "RecoverLostProfiles",
|
||||
"Translations": {
|
||||
"ar_SA": "الملفات الشخصية المفقودة",
|
||||
"de_DE": "Verlorene Profile",
|
||||
"el_GR": "Χαμένα προφίλ",
|
||||
"en_US": "Lost Profiles",
|
||||
"es_ES": "Perfiles Perdidos",
|
||||
"fr_FR": "Profils Perdus",
|
||||
"he_IL": "פרופילים אבודים",
|
||||
"it_IT": "Profili persi",
|
||||
"ja_JP": "失われたプロフィール",
|
||||
"ko_KR": "분실된 프로필",
|
||||
"no_NO": "Tapte profiler",
|
||||
"pl_PL": "Utracone profile",
|
||||
"pt_BR": "Perfis perdidos",
|
||||
"ru_RU": "Потерянные учёные записи",
|
||||
"sv_SE": "Förlorade profiler",
|
||||
"th_TH": "โปรไฟล์ที่สูญหาย",
|
||||
"tr_TR": "Kayıp profiller",
|
||||
"uk_UA": "Втрачені профілі",
|
||||
"zh_CN": "丢失的个人资料",
|
||||
"zh_TW": "遺失的個人資料"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "RecoverLostProfiles_ToolTip",
|
||||
"Translations": {
|
||||
"ar_SA": "يستعيد الملفات الشخصية التي لم تُحذف يدويًا والتي تحتوي على حفظات.",
|
||||
"de_DE": "Stellt nicht manuell gelöschte Profile mit Speicherständen wieder.",
|
||||
"el_GR": "Ανακτά προφίλ που δεν διαγράφηκαν χειροκίνητα και έχουν αποθηκεύσεις.",
|
||||
"en_US": "Recovers non-manually-deleted profiles that have saves.",
|
||||
"es_ES": "Recupera perfiles no eliminados manualmente que tienen guardados.",
|
||||
"fr_FR": "Récupère les profils non supprimés manuellement ayant des sauvegardes.",
|
||||
"he_IL": "שחזור פרופילים שלא נמחקו ידנית ויש להם שמירות.",
|
||||
"it_IT": "Recupera profili non eliminati manualmente che hanno salvataggi.",
|
||||
"ja_JP": "手動で削除されていない、保存されたプロフィールを回復します。",
|
||||
"ko_KR": "수동으로 삭제되지 않은 저장된 프로필을 복구합니다.",
|
||||
"no_NO": "Gjenoppretter profiler som ikke er manuelt slettet og som har lagringer.",
|
||||
"pl_PL": "Odzyskuje profile, które nie zostały usunięte ręcznie, a które mają zapisy.",
|
||||
"pt_BR": "Recupera perfis não deletados manualmente que possuem saves.",
|
||||
"ru_RU": "Восстанавливает учётные записи, не удалённые вручную и имеющие сохранения.",
|
||||
"sv_SE": "Återställer profiler som inte har raderats manuellt och har sparade data.",
|
||||
"th_TH": "กู้คืนโปรไฟล์ที่ไม่ได้ลบด้วยตนเองและมีการบันทึก",
|
||||
"tr_TR": "Manuel olarak silinmemiş ve kayıtlara sahip profilleri kurtarır.",
|
||||
"uk_UA": "Відновлює учётні записи, які не були видалені вручну і мають збереження.",
|
||||
"zh_CN": "恢复未手动删除且有存档的个人资料。",
|
||||
"zh_TW": "恢復未手動刪除且有存檔的個人資料。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "RecoverProfile",
|
||||
"Translations": {
|
||||
"ar_SA": "استعادة",
|
||||
"de_DE": "Wiederherstellen",
|
||||
"el_GR": "Ανάκτηση",
|
||||
"en_US": "Recover",
|
||||
"es_ES": "Recuperar",
|
||||
"fr_FR": "Récupérer",
|
||||
"he_IL": "שחזר",
|
||||
"it_IT": "Recupera",
|
||||
"ja_JP": "復旧",
|
||||
"ko_KR": "복구",
|
||||
"no_NO": "Gjenopprett",
|
||||
"pl_PL": "Odzyskaj",
|
||||
"pt_BR": "Recuperar",
|
||||
"ru_RU": "Восстановить",
|
||||
"sv_SE": "Återskapa",
|
||||
"th_TH": "กู้คืน",
|
||||
"tr_TR": "Kurtar",
|
||||
"uk_UA": "Відновити",
|
||||
"zh_CN": "恢复",
|
||||
"zh_TW": "復原"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "RecoverProfile_EmptyList",
|
||||
"Translations": {
|
||||
"ar_SA": "لا توجد ملفات شخصية لاستردادها",
|
||||
"de_DE": "Keine Profile zum Wiederherstellen",
|
||||
"el_GR": "Δεν υπάρχουν προφίλ για ανάκτηση",
|
||||
"en_US": "No Profiles To Recover",
|
||||
"es_ES": "No hay perfiles a recuperar",
|
||||
"fr_FR": "Aucun profil à restaurer",
|
||||
"he_IL": "אין פרופילים לשחזור",
|
||||
"it_IT": "Nessun profilo da recuperare",
|
||||
"ja_JP": "復元するプロファイルはありません",
|
||||
"ko_KR": "복구할 프로필 없음",
|
||||
"no_NO": "Ingen profiler å gjenopprette",
|
||||
"pl_PL": "Brak profili do odzyskania",
|
||||
"pt_BR": "Nenhum perfil para recuperar",
|
||||
"ru_RU": "Нет учётных записей для восстановления",
|
||||
"sv_SE": "Inga profiler att återskapa",
|
||||
"th_TH": "ไม่มีโปรไฟล์ที่สามารถกู้คืนได้",
|
||||
"tr_TR": "Kurtarılacak profil bulunamadı",
|
||||
"uk_UA": "Немає профілів для відновлення",
|
||||
"zh_CN": "没有可以恢复的用户数据",
|
||||
"zh_TW": "無設定檔可復原"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ManageSaves_SortByName",
|
||||
"Translations": {
|
||||
"ar_SA": "الاسم",
|
||||
"de_DE": "",
|
||||
"el_GR": "Όνομα",
|
||||
"en_US": "Name",
|
||||
"es_ES": "Nombre",
|
||||
"fr_FR": "Nom",
|
||||
"he_IL": "שם",
|
||||
"it_IT": "Nome",
|
||||
"ja_JP": "名称",
|
||||
"ko_KR": "이름",
|
||||
"no_NO": "Navn",
|
||||
"pl_PL": "Nazwa",
|
||||
"pt_BR": "Nome",
|
||||
"ru_RU": "Название",
|
||||
"sv_SE": "Namn",
|
||||
"th_TH": "ชื่อ",
|
||||
"tr_TR": "İsim",
|
||||
"uk_UA": "Назва",
|
||||
"zh_CN": "名称",
|
||||
"zh_TW": "名稱"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ManageSaves_SortBySize",
|
||||
"Translations": {
|
||||
"ar_SA": "الحجم",
|
||||
"de_DE": "Größe",
|
||||
"el_GR": "Μέγεθος",
|
||||
"en_US": "Size",
|
||||
"es_ES": "Tamaño",
|
||||
"fr_FR": "Taille",
|
||||
"he_IL": "גודל",
|
||||
"it_IT": "Dimensione",
|
||||
"ja_JP": "サイズ",
|
||||
"ko_KR": "크기",
|
||||
"no_NO": "Størrelse",
|
||||
"pl_PL": "Rozmiar",
|
||||
"pt_BR": "Tamanho",
|
||||
"ru_RU": "Размер",
|
||||
"sv_SE": "Storlek",
|
||||
"th_TH": "ขนาด",
|
||||
"tr_TR": "Boyut",
|
||||
"uk_UA": "Розмір",
|
||||
"zh_CN": "大小",
|
||||
"zh_TW": "大小"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ManageSaves_SortOrderAscending",
|
||||
"Translations": {
|
||||
"ar_SA": "تصاعدي",
|
||||
"de_DE": "Aufsteigend",
|
||||
"el_GR": "Αύξουσα",
|
||||
"en_US": "Ascending",
|
||||
"es_ES": "Ascendente",
|
||||
"fr_FR": "Croissant",
|
||||
"he_IL": "סדר עולה",
|
||||
"it_IT": "Crescente",
|
||||
"ja_JP": "昇順",
|
||||
"ko_KR": "오름차순",
|
||||
"no_NO": "Stigende",
|
||||
"pl_PL": "Rosnąco",
|
||||
"pt_BR": "Ascendente",
|
||||
"ru_RU": "По Возрастанию",
|
||||
"sv_SE": "Stigande",
|
||||
"th_TH": "จากน้อยไปมาก",
|
||||
"tr_TR": "Artan",
|
||||
"uk_UA": "За зростанням",
|
||||
"zh_CN": "升序",
|
||||
"zh_TW": "從小到大"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ManageSaves_SortOrderDescending",
|
||||
"Translations": {
|
||||
"ar_SA": "تنازلي",
|
||||
"de_DE": "Absteigend",
|
||||
"el_GR": "Φθίνουσα",
|
||||
"en_US": "Descending",
|
||||
"es_ES": "Descendente",
|
||||
"fr_FR": "Décroissant",
|
||||
"he_IL": "סדר יורד",
|
||||
"it_IT": "Decrescente",
|
||||
"ja_JP": "降順",
|
||||
"ko_KR": "내림차순",
|
||||
"no_NO": "Synkende",
|
||||
"pl_PL": "Malejąco",
|
||||
"pt_BR": "Descendente",
|
||||
"ru_RU": "По Убыванию",
|
||||
"sv_SE": "Fallande",
|
||||
"th_TH": "จากมากไปน้อย",
|
||||
"tr_TR": "Azalan",
|
||||
"uk_UA": "За спаданням",
|
||||
"zh_CN": "降序",
|
||||
"zh_TW": "從大到小"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ManageSaves_Search",
|
||||
"Translations": {
|
||||
"ar_SA": "بحث",
|
||||
"de_DE": "Suche",
|
||||
"el_GR": "Αναζήτηση",
|
||||
"en_US": "Search",
|
||||
"es_ES": "Buscar",
|
||||
"fr_FR": "Rechercher",
|
||||
"he_IL": "חפש",
|
||||
"it_IT": "Cerca",
|
||||
"ja_JP": "検索",
|
||||
"ko_KR": "찾기",
|
||||
"no_NO": "Søk",
|
||||
"pl_PL": "Wyszukaj",
|
||||
"pt_BR": "Buscar",
|
||||
"ru_RU": "Поиск",
|
||||
"sv_SE": "Sök",
|
||||
"th_TH": "ค้นหา",
|
||||
"tr_TR": "Ara",
|
||||
"uk_UA": "Пошук",
|
||||
"zh_CN": "搜索",
|
||||
"zh_TW": "搜尋"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "IrreversibleActionNote",
|
||||
"Translations": {
|
||||
"ar_SA": "هذا الإجراء لا يمكن التراجع عنه.",
|
||||
"de_DE": "Diese Option kann nicht rückgängig gemacht werden.",
|
||||
"el_GR": "Αυτή η ενέργεια είναι μη αναστρέψιμη.",
|
||||
"en_US": "This action is not reversible.",
|
||||
"es_ES": "Esta acción no es reversible.",
|
||||
"fr_FR": "Cette action n'est pas réversible.",
|
||||
"he_IL": "הפעולה הזו בלתי הפיכה.",
|
||||
"it_IT": "Questa azione non è reversibile.",
|
||||
"ja_JP": "この操作は元に戻せません.",
|
||||
"ko_KR": "이 작업은 되돌릴 수 없습니다.",
|
||||
"no_NO": "Denne handlingen er ikke reverserbar.",
|
||||
"pl_PL": "Ta czynność nie jest odwracalna.",
|
||||
"pt_BR": "Esta ação não é reversível.",
|
||||
"ru_RU": "Данное действие является необратимым.",
|
||||
"sv_SE": "Denna åtgärd går inte att ångra.",
|
||||
"th_TH": "การดำเนินการนี้ไม่สามารถย้อนกลับได้",
|
||||
"tr_TR": "Bu eylem geri alınamaz.",
|
||||
"uk_UA": "Цю дію не можна скасувати.",
|
||||
"zh_CN": "删除后不可恢复。",
|
||||
"zh_TW": "此動作將無法復原。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ButtonClose",
|
||||
"Translations": {
|
||||
"ar_SA": "إغلاق",
|
||||
"de_DE": "Schließen",
|
||||
"el_GR": "Κλείσιμο",
|
||||
"en_US": "Close",
|
||||
"es_ES": "Cerrar",
|
||||
"fr_FR": "Fermer",
|
||||
"he_IL": "סגירה",
|
||||
"it_IT": "Chiudi",
|
||||
"ja_JP": "閉じる",
|
||||
"ko_KR": "닫기",
|
||||
"no_NO": "Lukk",
|
||||
"pl_PL": "Zamknij",
|
||||
"pt_BR": "Fechar",
|
||||
"ru_RU": "Закрыть",
|
||||
"sv_SE": "Stäng",
|
||||
"th_TH": "ปิด",
|
||||
"tr_TR": "Kapat",
|
||||
"uk_UA": "Закрити",
|
||||
"zh_CN": "关闭",
|
||||
"zh_TW": "關閉"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "NameLabel",
|
||||
"Translations": {
|
||||
"ar_SA": "الاسم:",
|
||||
"de_DE": null,
|
||||
"el_GR": "Όνομα:",
|
||||
"en_US": "Name:",
|
||||
"es_ES": "Nombre:",
|
||||
"fr_FR": "Nom :",
|
||||
"he_IL": "שם:",
|
||||
"it_IT": "Nome:",
|
||||
"ja_JP": "名称:",
|
||||
"ko_KR": "이름 :",
|
||||
"no_NO": "Navn:",
|
||||
"pl_PL": "Nazwa:",
|
||||
"pt_BR": "Nome:",
|
||||
"ru_RU": "Имя:",
|
||||
"sv_SE": "Namn:",
|
||||
"th_TH": "ชื่อ:",
|
||||
"tr_TR": "İsim:",
|
||||
"uk_UA": "Імʼя",
|
||||
"zh_CN": "名称:",
|
||||
"zh_TW": "名稱:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ProfileNameSelectionWatermark",
|
||||
"Translations": {
|
||||
"ar_SA": "اختر اسم الملف الشخصي",
|
||||
"de_DE": "Wähle einen Profilnamen",
|
||||
"el_GR": "Επιλέξτε όνομα προφίλ",
|
||||
"en_US": "Choose a Profile Name",
|
||||
"es_ES": "Escoge un Nombre de Perfil",
|
||||
"fr_FR": "Choisir un Nom de Profil",
|
||||
"he_IL": "בחרו שם פרופיל",
|
||||
"it_IT": "Scegli un Nome Profilo",
|
||||
"ja_JP": "プロフィール名を選択",
|
||||
"ko_KR": "프로필 이름 선택",
|
||||
"no_NO": "Velg et Profilnavn",
|
||||
"pl_PL": "Wybierz nazwę profilu",
|
||||
"pt_BR": "Escolha um Nome de Perfil",
|
||||
"ru_RU": "Выберите имя профиля",
|
||||
"sv_SE": "Välj ett Profilnamn",
|
||||
"th_TH": "เลือก ชื่อโปรไฟล์",
|
||||
"tr_TR": "Profil Adı Seç",
|
||||
"uk_UA": "Оберіть ім'я профілю",
|
||||
"zh_CN": "选择个人资料名称",
|
||||
"zh_TW": "選擇個人資料名稱"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "UserIdLabel",
|
||||
"Translations": {
|
||||
"ar_SA": "معرف المستخدم:",
|
||||
"de_DE": "Benutzer-ID:",
|
||||
"el_GR": "Ταυτότητα Χρήστη:",
|
||||
"en_US": "User ID:",
|
||||
"es_ES": "ID de Usuario:",
|
||||
"fr_FR": "Identifiant Utilisateur :",
|
||||
"he_IL": "מזהה משתמש:",
|
||||
"it_IT": "ID utente:",
|
||||
"ja_JP": "ユーザID:",
|
||||
"ko_KR": "사용자 ID :",
|
||||
"no_NO": "Bruker ID:",
|
||||
"pl_PL": "ID Użytkownika:",
|
||||
"pt_BR": "ID de Usuário:",
|
||||
"ru_RU": "ID пользователя:",
|
||||
"sv_SE": "Användar-id:",
|
||||
"th_TH": "รหัสผู้ใช้:",
|
||||
"tr_TR": "Kullanıcı ID:",
|
||||
"uk_UA": "ID користувача:",
|
||||
"zh_CN": "用户 ID:",
|
||||
"zh_TW": "使用者 ID:"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ProfileImage_Import",
|
||||
"Translations": {
|
||||
"ar_SA": "استيراد الصورة",
|
||||
"de_DE": "Bild importieren",
|
||||
"el_GR": "Εισαγωγή Εικόνας",
|
||||
"en_US": "Import Image",
|
||||
"es_ES": "Importar Imagen",
|
||||
"fr_FR": "Importer une image",
|
||||
"he_IL": "ייבוא תמונה",
|
||||
"it_IT": "Importa immagine",
|
||||
"ja_JP": "画像をインポート",
|
||||
"ko_KR": "이미지 가져오기",
|
||||
"no_NO": "Importer bilde",
|
||||
"pl_PL": "Importuj obraz",
|
||||
"pt_BR": "Importar Imagem",
|
||||
"ru_RU": "Импорт изображения",
|
||||
"sv_SE": "Importera bild",
|
||||
"th_TH": "นำเข้าภาพ",
|
||||
"tr_TR": "Resim İçeri Aktar",
|
||||
"uk_UA": "Імпорт зображення",
|
||||
"zh_CN": "导入图像",
|
||||
"zh_TW": "匯入圖像"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ProfileImage_SelectAvatar",
|
||||
"Translations": {
|
||||
"ar_SA": "حدد صورة الأفاتار من البرنامج الثابت",
|
||||
"de_DE": "Firmware-Avatar auswählen",
|
||||
"el_GR": "Επιλέξτε Avatar από Firmware",
|
||||
"en_US": "Select Firmware Avatar",
|
||||
"es_ES": "Seleccionar Avatar del Firmware",
|
||||
"fr_FR": "Choisir un Avatar du Firmware",
|
||||
"he_IL": "בחרו אוואטר קושחה",
|
||||
"it_IT": "Seleziona avatar dal firmware",
|
||||
"ja_JP": "ファームウェア内のアバターを選択",
|
||||
"ko_KR": "펌웨어 아바타 선택",
|
||||
"no_NO": "Velg firmware-avatar",
|
||||
"pl_PL": "Wybierz avatar z oprogramowania",
|
||||
"pt_BR": "Selecionar Avatar do Firmware",
|
||||
"ru_RU": "Выбрать аватар прошивки",
|
||||
"sv_SE": "Välj avatar från firmware",
|
||||
"th_TH": "เลือกอวาต้าจากระบบ",
|
||||
"tr_TR": "Yazılım Avatarı Seç",
|
||||
"uk_UA": "Виберіть аватар прошивки",
|
||||
"zh_CN": "选择固件头像",
|
||||
"zh_TW": "選取韌體大頭貼"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SupportedImageFormatDialogTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "اختر إما JPG أو JPEG أو PNG أو BMP",
|
||||
"de_DE": "Wählen Sie entweder ein JPG, JPEG, PNG oder BMP",
|
||||
"el_GR": "Επιλέξτε είτε JPG, JPEG, PNG ή BMP",
|
||||
"en_US": "Choose either a JPG, JPEG, PNG, or BMP",
|
||||
"es_ES": "Elige ya sea JPG, JPEG, PNG o BMP",
|
||||
"fr_FR": "Choisissez soit un JPG, JPEG, PNG ou BMP",
|
||||
"he_IL": "בחר את JPG, JPEG, PNG או BMP",
|
||||
"it_IT": "Scegli tra JPG, JPEG, PNG o BMP",
|
||||
"ja_JP": "JPG、JPEG、PNG、またはBMPのいずれかを選択してください",
|
||||
"ko_KR": "JPG, JPEG, PNG 또는 BMP 중에서 선택하세요",
|
||||
"no_NO": "Velg enten et JPG, JPEG, PNG eller BMP",
|
||||
"pl_PL": "Wybierz JPG, JPEG, PNG lub BMP",
|
||||
"pt_BR": "Escolha JPG, JPEG, PNG ou BMP",
|
||||
"ru_RU": "Выберите либо JPG, JPEG, PNG, или BMP",
|
||||
"sv_SE": "Välj antingen ett JPG, JPEG, PNG eller BMP",
|
||||
"th_TH": "เลือก JPG, JPEG, PNG หรือ BMP",
|
||||
"tr_TR": "JPG, JPEG, PNG veya BMP seçin",
|
||||
"uk_UA": "Виберіть або JPG, JPEG, PNG, або BMP",
|
||||
"zh_CN": "选择 JPG、JPEG、PNG 或 BMP",
|
||||
"zh_TW": "選擇 JPG、JPEG、PNG 或 BMP"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "SelectAvatarTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "تحديد أفاتار البرنامج الثابت",
|
||||
"de_DE": "Firmware-Avatar auswählen",
|
||||
"el_GR": "Επιλογή Avatar Firmware",
|
||||
"en_US": "Select Firmware Avatar",
|
||||
"es_ES": "Seleccionar Avatar del Firmware",
|
||||
"fr_FR": "Sélection d’un Avatar du Firmware",
|
||||
"he_IL": "בחירת אוואטר קושחה",
|
||||
"it_IT": "Selezione Avatar Firmware",
|
||||
"ja_JP": "ファームウェアアバター選択",
|
||||
"ko_KR": "펌웨어 아바타 선택",
|
||||
"no_NO": "Velg firmware-avatar",
|
||||
"pl_PL": "Wybór awatara oprogramowania",
|
||||
"pt_BR": "Selecionar Avatar do Firmware",
|
||||
"ru_RU": "Выбор аватара прошивки",
|
||||
"sv_SE": "Välj firmware-avatar",
|
||||
"th_TH": "การเลือกอวตารเฟิร์มแวร์",
|
||||
"tr_TR": "Firmware Avatar Seçimi",
|
||||
"uk_UA": "Вибір аватара прошивки",
|
||||
"zh_CN": "选择固件头像",
|
||||
"zh_TW": "選取韌體頭像"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ButtonChooseAvatar",
|
||||
"Translations": {
|
||||
"ar_SA": "اختر الأفاتار",
|
||||
"de_DE": "Wähle Avatar",
|
||||
"el_GR": "Επιλέξτε Avatar",
|
||||
"en_US": "Choose Avatar",
|
||||
"es_ES": "Elegir Avatar",
|
||||
"fr_FR": "Choisir un Avatar",
|
||||
"he_IL": "בחרו אוואטר",
|
||||
"it_IT": "Scegli Avatar",
|
||||
"ja_JP": "アバターを選択",
|
||||
"ko_KR": "아바타 선택",
|
||||
"no_NO": "Velg avatar",
|
||||
"pl_PL": "Wybierz awatar",
|
||||
"pt_BR": "Escolher Avatar",
|
||||
"ru_RU": "Выбрать аватар",
|
||||
"sv_SE": "Välj avatar",
|
||||
"th_TH": "เลือกอวาต้าของคุณ",
|
||||
"tr_TR": "Avatar Seç",
|
||||
"uk_UA": "Вибрати аватар",
|
||||
"zh_CN": "选择头像",
|
||||
"zh_TW": "選擇大頭貼"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DialogUserProfileUnsavedChangesMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "لقد قمت بإجراء تغييرات غير محفوظة على هذا الملف الشخصي.",
|
||||
"de_DE": "Sie haben nicht gespeicherte Änderungen an diesem Profil.",
|
||||
"el_GR": "Έχετε μη αποθηκευμένες αλλαγές σε αυτό το προφίλ.",
|
||||
"en_US": "You have unsaved changes to this profile.",
|
||||
"es_ES": "Tienes cambios no guardados en este perfil.",
|
||||
"fr_FR": "Vous avez des modifications non enregistrées sur ce profil.",
|
||||
"he_IL": "ביצעת שינויים לא שמורים בפרופיל זה.",
|
||||
"it_IT": "Hai modifiche non salvate su questo profilo.",
|
||||
"ja_JP": "このプロファイルには保存されていない変更があります.",
|
||||
"ko_KR": "이 프로필에는 저장되지 않은 변경 사항이 있습니다.",
|
||||
"no_NO": "Du har usparende endringer på denne profilen.",
|
||||
"pl_PL": "Masz niezapisane zmiany w tym profilu.",
|
||||
"pt_BR": "Você tem alterações não salvas neste perfil.",
|
||||
"ru_RU": "У вас есть несохраненные изменения в этом профиле.",
|
||||
"sv_SE": "Du har osparade ändringar i den här profilen.",
|
||||
"th_TH": "คุณมีการเปลี่ยนแปลงที่ยังไม่ได้บันทึกในโปรไฟล์นี้",
|
||||
"tr_TR": "Bu profilde kaydedilmemiş değişiklikleriniz var.",
|
||||
"uk_UA": "У вас є незбережені зміни в цьому профілі.",
|
||||
"zh_CN": "您对该账户有未保存的更改。",
|
||||
"zh_TW": "您對該使用者設定檔有未儲存的變更。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DialogUserProfileUnsavedChangesSubMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "هل تريد تجاهل التغييرات؟",
|
||||
"de_DE": "Verwerfen Sie die Änderungen?",
|
||||
"el_GR": "Θέλετε να απορρίψετε τις αλλαγές?",
|
||||
"en_US": "Discard changes?",
|
||||
"es_ES": "¿Descartar los cambios?",
|
||||
"fr_FR": "Annuler les modifications ?",
|
||||
"he_IL": "האם ברצונך להתעלם מהשינויים?",
|
||||
"it_IT": "Scartare le modifiche?",
|
||||
"ja_JP": "変更を破棄しますか?",
|
||||
"ko_KR": "변경 사항을 취소하시겠습니까?",
|
||||
"no_NO": "Vil du forkaste endringene?",
|
||||
"pl_PL": "Czy chcesz odrzucić zmiany?",
|
||||
"pt_BR": "Deseja descartar as alterações?",
|
||||
"ru_RU": "Отменить изменения?",
|
||||
"sv_SE": "Vill du förkasta ändringarna?",
|
||||
"th_TH": "คุณต้องการทิ้งการเปลี่ยนแปลงหรือไม่?",
|
||||
"tr_TR": "Değişiklikleri iptal et?",
|
||||
"uk_UA": "Бажаєте скасувати зміни?",
|
||||
"zh_CN": "确定要放弃更改吗?",
|
||||
"zh_TW": "您確定要放棄變更嗎?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DialogUserProfileUnsavedChangesTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "تحذير - التغييرات غير محفوظة",
|
||||
"de_DE": "WARNUNG - Nicht gespeicherte Änderungen",
|
||||
"el_GR": "ΠΡΟΣΟΧΗ - Μην Αποθηκευμένες Αλλαγές.",
|
||||
"en_US": "WARNING - Unsaved Changes",
|
||||
"es_ES": "ADVERTENCIA - Cambios Sin Guardar",
|
||||
"fr_FR": "AVERTISSEMENT - Modifications Non Enregistrées",
|
||||
"he_IL": "אזהרה - שינויים לא שמורים",
|
||||
"it_IT": "ATTENZIONE - Modifiche non salvate",
|
||||
"ja_JP": "警告 - 保存されていない変更",
|
||||
"ko_KR": "경고 - 저장되지 않은 변경 사항",
|
||||
"no_NO": "ADVARSEL - Ulagrede endringer",
|
||||
"pl_PL": "UWAGA - Niezapisane zmiany",
|
||||
"pt_BR": "ALERTA - Alterações não salvas",
|
||||
"ru_RU": "ВНИМАНИЕ - Несохраненные изменения",
|
||||
"sv_SE": "VARNING - Ej sparade ändringar",
|
||||
"th_TH": "คำเตือน - มีการเปลี่ยนแปลงที่ไม่ได้บันทึก",
|
||||
"tr_TR": "UYARI - Kaydedilmemiş Değişiklikler",
|
||||
"uk_UA": "УВАГА — Незбережені зміни",
|
||||
"zh_CN": "警告 - 有未保存的更改",
|
||||
"zh_TW": "警告 - 未儲存的變更"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DialogUserProfileDeletionConfirmMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "هل حذف الملف الشخصي المحدد؟",
|
||||
"de_DE": "Löschen Sie das ausgewählte Profil?",
|
||||
"el_GR": "Διαγραφή του επιλεγμένου προφίλ;",
|
||||
"en_US": "Delete the selected profile?",
|
||||
"es_ES": "¿Eliminar el perfil seleccionado?",
|
||||
"fr_FR": "Supprimer le profil sélectionné ?",
|
||||
"he_IL": "האם למחוק את הפרופיל שנבחר?",
|
||||
"it_IT": "Eliminare il profilo selezionato?",
|
||||
"ja_JP": "選択されたプロファイルを削除しますか?",
|
||||
"ko_KR": "선택한 프로필을 삭제하시겠습니까?",
|
||||
"no_NO": "Slette den valgte profilen?",
|
||||
"pl_PL": "Usunąć wybrany profil?",
|
||||
"pt_BR": "Excluir o perfil selecionado?",
|
||||
"ru_RU": "Удалить выбранный профиль?",
|
||||
"sv_SE": "Ta bort den valda profilen?",
|
||||
"th_TH": "ลบโปรไฟล์ที่เลือก?",
|
||||
"tr_TR": "Seçilen profili silmek?",
|
||||
"uk_UA": "Видалити вибраний профіль?",
|
||||
"zh_CN": "删除所选账户?",
|
||||
"zh_TW": "刪除所選設定檔?"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "DialogUserProfileDeletionWarningMessage",
|
||||
"Translations": {
|
||||
"ar_SA": "لن تكون هناك ملفات الشخصية أخرى لفتحها إذا تم حذف الملف الشخصي المحدد.",
|
||||
"de_DE": "Es können keine anderen Profile geöffnet werden, wenn das ausgewählte Profil gelöscht wird.",
|
||||
"el_GR": "Δεν θα υπάρχουν άλλα προφίλ εάν διαγραφεί το επιλεγμένο.",
|
||||
"en_US": "There would be no other profiles to be opened if selected profile is deleted.",
|
||||
"es_ES": "Si eliminas el perfil seleccionado no quedará ningún otro perfil.",
|
||||
"fr_FR": "Il n'y aurait aucun autre profil à ouvrir si le profil sélectionné est supprimé.",
|
||||
"he_IL": "לא יהיו פרופילים אחרים שייפתחו אם הפרופיל שנבחר יימחק.",
|
||||
"it_IT": "Non ci sarebbero altri profili da aprire se il profilo selezionato venisse cancellato.",
|
||||
"ja_JP": "選択されたプロファイルを削除すると,プロファイルがひとつも存在しなくなります.",
|
||||
"ko_KR": "선택한 프로필을 삭제하면 다른 프로필을 열 수 없음.",
|
||||
"no_NO": "Det vil ikke være noen profiler å åpnes hvis valgt profil blir slettet.",
|
||||
"pl_PL": "Nie będzie innych profili do otwarcia, jeśli wybrany profil zostanie usunięty.",
|
||||
"pt_BR": "Não haveria nenhum perfil selecionado se o perfil atual fosse deletado.",
|
||||
"ru_RU": "Если выбранный профиль будет удален, другие профили не будут открываться.",
|
||||
"sv_SE": "Det skulle inte finnas några andra profiler att öppnas om angiven profil tas bort.",
|
||||
"th_TH": "จะไม่มีโปรไฟล์อื่นให้เปิดหากโปรไฟล์ที่เลือกถูกลบ",
|
||||
"tr_TR": "Seçilen profil silinirse kullanılabilen başka profil kalmayacak.",
|
||||
"uk_UA": "Якщо вибраний профіль буде видалено, інші профілі не відкриватимуться.",
|
||||
"zh_CN": "删除后将没有可用的账户。",
|
||||
"zh_TW": "如果刪除選取的設定檔,將無法開啟其他設定檔。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ButtonDelete",
|
||||
"Translations": {
|
||||
"ar_SA": "حذف",
|
||||
"de_DE": "Löschen",
|
||||
"el_GR": "Διαγράφω",
|
||||
"en_US": "Delete",
|
||||
"es_ES": "Eliminar",
|
||||
"fr_FR": "Supprimer",
|
||||
"he_IL": "מחיקה",
|
||||
"it_IT": "Elimina",
|
||||
"ja_JP": "削除",
|
||||
"ko_KR": "삭제",
|
||||
"no_NO": "Slett",
|
||||
"pl_PL": "Usuń",
|
||||
"pt_BR": "Apagar",
|
||||
"ru_RU": "Удалить",
|
||||
"sv_SE": "Ta bort",
|
||||
"th_TH": "ลบ",
|
||||
"tr_TR": "Sil",
|
||||
"uk_UA": "Видалити",
|
||||
"zh_CN": "删除",
|
||||
"zh_TW": "刪除"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "ButtonSave",
|
||||
"Translations": {
|
||||
"ar_SA": "حفظ",
|
||||
"de_DE": "Speichern",
|
||||
"el_GR": "Αποθήκευση",
|
||||
"en_US": "Save",
|
||||
"es_ES": "Guardar",
|
||||
"fr_FR": "Enregistrer",
|
||||
"he_IL": "שמור",
|
||||
"it_IT": "Salva",
|
||||
"ja_JP": "セーブ",
|
||||
"ko_KR": "저장",
|
||||
"no_NO": "Lagre",
|
||||
"pl_PL": "Zapisz",
|
||||
"pt_BR": "Salvar",
|
||||
"ru_RU": "Сохранить",
|
||||
"sv_SE": "Spara",
|
||||
"th_TH": "บันทึก",
|
||||
"tr_TR": "Kaydet",
|
||||
"uk_UA": "Зберегти",
|
||||
"zh_CN": "保存",
|
||||
"zh_TW": "儲存"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "UserEditorTitle",
|
||||
"Translations": {
|
||||
"ar_SA": "جارٍ تعديل {0}",
|
||||
"de_DE": "{0} wird bearbeitet",
|
||||
"el_GR": "Επεξεργασία {0}",
|
||||
"en_US": "Editing {0}",
|
||||
"es_ES": "Editando {0}",
|
||||
"fr_FR": "Modification de {0}",
|
||||
"he_IL": "עריכת {0}",
|
||||
"it_IT": "Modifica di {0}",
|
||||
"ja_JP": "{0} を編集中",
|
||||
"ko_KR": "{0} 편집 중",
|
||||
"no_NO": "Redigerer {0}",
|
||||
"pl_PL": "Edycja {0}",
|
||||
"pt_BR": "Editando {0}",
|
||||
"ru_RU": "Редактирование {0}",
|
||||
"sv_SE": "Redigerar {0}",
|
||||
"th_TH": "กำลังกำลังแก้ไข {0}",
|
||||
"tr_TR": "{0} düzenleniyor",
|
||||
"uk_UA": "Редагування {0}",
|
||||
"zh_CN": "正在编辑 {0}",
|
||||
"zh_TW": "正在編輯 {0}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "UserEditorTitleNewUser",
|
||||
"Translations": {
|
||||
"ar_SA": "مستخدم جديد",
|
||||
"de_DE": "Neuer Nutzer",
|
||||
"el_GR": "Νέος Χρήστης",
|
||||
"en_US": "New User",
|
||||
"es_ES": "Nuevo Usuario",
|
||||
"fr_FR": "Nouvel Utilisateur",
|
||||
"he_IL": "משתמש חדש",
|
||||
"it_IT": "Nuovo utente",
|
||||
"ja_JP": "新しいユーザー",
|
||||
"ko_KR": "새 사용자",
|
||||
"no_NO": "Ny bruker",
|
||||
"pl_PL": "Nowy użytkownik",
|
||||
"pt_BR": "Novo usuário",
|
||||
"ru_RU": "Новый пользователь",
|
||||
"sv_SE": "Ny användare",
|
||||
"th_TH": "ผู้ใช้ใหม่",
|
||||
"tr_TR": "Yeni kullanıcı",
|
||||
"uk_UA": "Новий користувач",
|
||||
"zh_CN": "新用户",
|
||||
"zh_TW": "新使用者"
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID": "EmptyNameError",
|
||||
"Translations": {
|
||||
"ar_SA": "الاسم مطلوب",
|
||||
"de_DE": "Name ist erforderlich",
|
||||
"el_GR": "Απαιτείται όνομα",
|
||||
"en_US": "Name is required",
|
||||
"es_ES": "El nombre es obligatorio",
|
||||
"fr_FR": "Le nom est requis",
|
||||
"he_IL": "נדרש שם",
|
||||
"it_IT": "Il nome è obbligatorio",
|
||||
"ja_JP": "名称が必要です",
|
||||
"ko_KR": "이름 필수 입력",
|
||||
"no_NO": "Navn er påkrevd",
|
||||
"pl_PL": "Nazwa jest wymagana",
|
||||
"pt_BR": "Nome é obrigatório",
|
||||
"ru_RU": "Необходимо ввести имя",
|
||||
"sv_SE": "Namn krävs",
|
||||
"th_TH": "จำเป็นต้องระบุชื่อ",
|
||||
"tr_TR": "İsim gerekli",
|
||||
"uk_UA": "Імʼя обовʼязкове",
|
||||
"zh_CN": "必须输入名称",
|
||||
"zh_TW": "名稱為必填"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -2050,7 +2050,9 @@
|
||||
010003C00B868000,"Ninjin: Clash of Carrots",online-broken,playable,2024-07-10 05:12:26
|
||||
0100746010E4C000,"NinNinDays",,playable,2022-11-20 15:17:29
|
||||
0100C9A00ECE6000,"Nintendo 64™ – Nintendo Switch Online",gpu;vulkan,ingame,2024-04-23 20:21:07
|
||||
010057D00ECE4000,"Nintendo 64™ – Nintendo Switch Online",gpu;vulkan,ingame,2024-04-23 20:21:07
|
||||
0100e0601c632000,"Nintendo 64™ – Nintendo Switch Online: MATURE 17+",,ingame,2025-02-03 22:27:00
|
||||
010037A0170D2000,"NINTENDO 64™ – Nintendo Switch Online 18+",,ingame,2025-02-03 22:27:00
|
||||
0100D870045B6000,"Nintendo Entertainment System™ - Nintendo Switch Online",online,playable,2022-07-01 15:45:06
|
||||
0100C4B0034B2000,"Nintendo Labo Toy-Con 01 Variety Kit",gpu,ingame,2022-08-07 12:56:07
|
||||
01001E9003502000,"Nintendo Labo Toy-Con 03 Vehicle Kit",services;crash,menus,2022-08-03 17:20:11
|
||||
@@ -2638,6 +2640,7 @@
|
||||
0100B16009C10000,"SINNER: Sacrifice for Redemption",nvdec;UE4;vulkan-backend-bug,playable,2022-08-12 20:37:33
|
||||
0100E9201410E000,"Sir Lovelot",,playable,2021-04-05 16:21:46
|
||||
0100134011E32000,"Skate City",,playable,2022-11-04 11:37:39
|
||||
0100a8501b66e000,"Skateboard Drifting with Maxwell Cat: The Game Simulator",,playable,2026-02-17 19:05:00
|
||||
0100B2F008BD8000,"Skee-Ball",,playable,2020-11-16 04:44:07
|
||||
01001A900F862000,"Skelattack",,playable,2021-06-09 15:26:26
|
||||
01008E700F952000,"Skelittle: A Giant Party!",,playable,2021-06-09 19:08:34
|
||||
@@ -3307,6 +3310,7 @@
|
||||
0100AFA011068000,"Voxel Pirates",,playable,2022-09-28 22:55:02
|
||||
0100BFB00D1F4000,"Voxel Sword",,playable,2022-08-30 14:57:27
|
||||
01004E90028A2000,"Vroom in the night sky",Needs Update;vulkan-backend-bug,playable,2023-02-20 02:32:29
|
||||
0100BFC01D976000,"Virtual Boy – Nintendo Classics",services,nothing,2026-02-17 11:26:59
|
||||
0100C7C00AE6C000,"VSR: Void Space Racing",,playable,2021-01-27 14:08:59
|
||||
0100B130119D0000,"Waifu Uncovered",crash,ingame,2023-02-27 01:17:46
|
||||
0100E29010A4A000,"Wanba Warriors",,playable,2020-10-04 17:56:22
|
||||
|
||||
|
@@ -168,7 +168,7 @@ namespace ARMeilleure.Common
|
||||
{
|
||||
_allocated.Dispose();
|
||||
|
||||
foreach (IntPtr page in _pages.Values)
|
||||
foreach (nint page in _pages.Values)
|
||||
{
|
||||
NativeAllocator.Instance.Free((void*)page);
|
||||
}
|
||||
|
||||
16
src/Ryujinx.Audio.Backends.Apple/AppleAudioBuffer.cs
Normal file
16
src/Ryujinx.Audio.Backends.Apple/AppleAudioBuffer.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Ryujinx.Audio.Backends.Apple
|
||||
{
|
||||
class AppleAudioBuffer
|
||||
{
|
||||
public readonly ulong DriverIdentifier;
|
||||
public readonly ulong SampleCount;
|
||||
public ulong SamplePlayed;
|
||||
|
||||
public AppleAudioBuffer(ulong driverIdentifier, ulong sampleCount)
|
||||
{
|
||||
DriverIdentifier = driverIdentifier;
|
||||
SampleCount = sampleCount;
|
||||
SamplePlayed = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
196
src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs
Normal file
196
src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceDriver.cs
Normal file
@@ -0,0 +1,196 @@
|
||||
using Ryujinx.Audio.Common;
|
||||
using Ryujinx.Audio.Integration;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Memory;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Runtime.Versioning;
|
||||
using Ryujinx.Audio.Backends.Apple.Native;
|
||||
using static Ryujinx.Audio.Backends.Apple.Native.AudioToolbox;
|
||||
using static Ryujinx.Audio.Integration.IHardwareDeviceDriver;
|
||||
|
||||
namespace Ryujinx.Audio.Backends.Apple
|
||||
{
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("ios")]
|
||||
public sealed class AppleHardwareDeviceDriver : IHardwareDeviceDriver
|
||||
{
|
||||
private readonly ManualResetEvent _updateRequiredEvent;
|
||||
private readonly ManualResetEvent _pauseEvent;
|
||||
private readonly ConcurrentDictionary<AppleHardwareDeviceSession, byte> _sessions;
|
||||
private readonly bool _supportSurroundConfiguration;
|
||||
|
||||
public float Volume { get; set; }
|
||||
|
||||
public AppleHardwareDeviceDriver()
|
||||
{
|
||||
_updateRequiredEvent = new ManualResetEvent(false);
|
||||
_pauseEvent = new ManualResetEvent(true);
|
||||
_sessions = new ConcurrentDictionary<AppleHardwareDeviceSession, byte>();
|
||||
|
||||
_supportSurroundConfiguration = TestSurroundSupport();
|
||||
|
||||
Volume = 1f;
|
||||
}
|
||||
|
||||
private bool TestSurroundSupport()
|
||||
{
|
||||
try
|
||||
{
|
||||
AudioStreamBasicDescription format =
|
||||
GetAudioFormat(SampleFormat.PcmFloat, Constants.TargetSampleRate, 6);
|
||||
|
||||
int result = AudioQueueNewOutput(
|
||||
ref format,
|
||||
nint.Zero,
|
||||
nint.Zero,
|
||||
nint.Zero,
|
||||
nint.Zero,
|
||||
0,
|
||||
out nint testQueue);
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
AudioChannelLayout layout = new AudioChannelLayout
|
||||
{
|
||||
AudioChannelLayoutTag = kAudioChannelLayoutTag_MPEG_5_1_A,
|
||||
AudioChannelBitmap = 0,
|
||||
NumberChannelDescriptions = 0
|
||||
};
|
||||
|
||||
int layoutResult = AudioQueueSetProperty(
|
||||
testQueue,
|
||||
kAudioQueueProperty_ChannelLayout,
|
||||
ref layout,
|
||||
(uint)Marshal.SizeOf<AudioChannelLayout>());
|
||||
|
||||
if (layoutResult == 0)
|
||||
{
|
||||
AudioQueueDispose(testQueue, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
AudioQueueDispose(testQueue, true);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsSupported => OperatingSystem.IsMacOSVersionAtLeast(10, 5);
|
||||
|
||||
public ManualResetEvent GetUpdateRequiredEvent()
|
||||
=> _updateRequiredEvent;
|
||||
|
||||
public ManualResetEvent GetPauseEvent()
|
||||
=> _pauseEvent;
|
||||
|
||||
public IHardwareDeviceSession OpenDeviceSession(Direction direction, IVirtualMemoryManager memoryManager,
|
||||
SampleFormat sampleFormat, uint sampleRate, uint channelCount)
|
||||
{
|
||||
if (channelCount == 0)
|
||||
{
|
||||
channelCount = 2;
|
||||
}
|
||||
|
||||
if (sampleRate == 0)
|
||||
{
|
||||
sampleRate = Constants.TargetSampleRate;
|
||||
}
|
||||
|
||||
if (direction != Direction.Output)
|
||||
{
|
||||
throw new NotImplementedException("Input direction is currently not implemented on Apple backend!");
|
||||
}
|
||||
|
||||
AppleHardwareDeviceSession session = new(this, memoryManager, sampleFormat, sampleRate, channelCount);
|
||||
|
||||
_sessions.TryAdd(session, 0);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
internal bool Unregister(AppleHardwareDeviceSession session)
|
||||
=> _sessions.TryRemove(session, out _);
|
||||
|
||||
internal static AudioStreamBasicDescription GetAudioFormat(SampleFormat sampleFormat, uint sampleRate,
|
||||
uint channelCount)
|
||||
{
|
||||
uint formatFlags;
|
||||
uint bitsPerChannel;
|
||||
|
||||
switch (sampleFormat)
|
||||
{
|
||||
case SampleFormat.PcmInt8:
|
||||
formatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
||||
bitsPerChannel = 8;
|
||||
break;
|
||||
case SampleFormat.PcmInt16:
|
||||
formatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
||||
bitsPerChannel = 16;
|
||||
break;
|
||||
case SampleFormat.PcmInt32:
|
||||
formatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
|
||||
bitsPerChannel = 32;
|
||||
break;
|
||||
case SampleFormat.PcmFloat:
|
||||
formatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked;
|
||||
bitsPerChannel = 32;
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"Unsupported sample format {sampleFormat}");
|
||||
}
|
||||
|
||||
uint bytesPerFrame = (bitsPerChannel / 8) * channelCount;
|
||||
|
||||
return new AudioStreamBasicDescription
|
||||
{
|
||||
SampleRate = sampleRate,
|
||||
FormatID = kAudioFormatLinearPCM,
|
||||
FormatFlags = formatFlags,
|
||||
BytesPerPacket = bytesPerFrame,
|
||||
FramesPerPacket = 1,
|
||||
BytesPerFrame = bytesPerFrame,
|
||||
ChannelsPerFrame = channelCount,
|
||||
BitsPerChannel = bitsPerChannel,
|
||||
Reserved = 0
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
foreach (AppleHardwareDeviceSession session in _sessions.Keys)
|
||||
{
|
||||
session.Dispose();
|
||||
}
|
||||
|
||||
_pauseEvent.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public bool SupportsDirection(Direction direction)
|
||||
=> direction != Direction.Input;
|
||||
|
||||
public bool SupportsSampleRate(uint sampleRate) => true;
|
||||
|
||||
public bool SupportsSampleFormat(SampleFormat sampleFormat)
|
||||
=> sampleFormat != SampleFormat.PcmInt24;
|
||||
|
||||
public bool SupportsChannelCount(uint channelCount)
|
||||
=> channelCount != 6 || _supportSurroundConfiguration;
|
||||
}
|
||||
}
|
||||
285
src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs
Normal file
285
src/Ryujinx.Audio.Backends.Apple/AppleHardwareDeviceSession.cs
Normal file
@@ -0,0 +1,285 @@
|
||||
using Ryujinx.Audio.Backends.Common;
|
||||
using Ryujinx.Audio.Common;
|
||||
using Ryujinx.Memory;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Runtime.Versioning;
|
||||
using static Ryujinx.Audio.Backends.Apple.Native.AudioToolbox;
|
||||
|
||||
namespace Ryujinx.Audio.Backends.Apple
|
||||
{
|
||||
[SupportedOSPlatform("macos")]
|
||||
[SupportedOSPlatform("ios")]
|
||||
class AppleHardwareDeviceSession : HardwareDeviceSessionOutputBase
|
||||
{
|
||||
private const int NumBuffers = 3;
|
||||
|
||||
private readonly AppleHardwareDeviceDriver _driver;
|
||||
private readonly ConcurrentQueue<AppleAudioBuffer> _queuedBuffers = new();
|
||||
private readonly DynamicRingBuffer _ringBuffer = new();
|
||||
private readonly ManualResetEvent _updateRequiredEvent;
|
||||
|
||||
private readonly AudioQueueOutputCallback _callbackDelegate;
|
||||
private readonly GCHandle _gcHandle;
|
||||
|
||||
private nint _audioQueue;
|
||||
private readonly nint[] _audioQueueBuffers = new nint[NumBuffers];
|
||||
private readonly int[] _bufferBytesFilled = new int[NumBuffers];
|
||||
|
||||
private readonly int _bytesPerFrame;
|
||||
|
||||
private ulong _playedSampleCount;
|
||||
private bool _started;
|
||||
private float _volume = 1f;
|
||||
|
||||
private readonly object _lock = new();
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate void AudioQueueOutputCallback(
|
||||
nint userData,
|
||||
nint audioQueue,
|
||||
nint buffer);
|
||||
|
||||
public AppleHardwareDeviceSession(
|
||||
AppleHardwareDeviceDriver driver,
|
||||
IVirtualMemoryManager memoryManager,
|
||||
SampleFormat requestedSampleFormat,
|
||||
uint requestedSampleRate,
|
||||
uint requestedChannelCount)
|
||||
: base(memoryManager, requestedSampleFormat, requestedSampleRate, requestedChannelCount)
|
||||
{
|
||||
_driver = driver;
|
||||
_updateRequiredEvent = driver.GetUpdateRequiredEvent();
|
||||
_callbackDelegate = OutputCallback;
|
||||
_bytesPerFrame = BackendHelper.GetSampleSize(requestedSampleFormat) * (int)requestedChannelCount;
|
||||
|
||||
_gcHandle = GCHandle.Alloc(this, GCHandleType.Normal);
|
||||
|
||||
SetupAudioQueue();
|
||||
}
|
||||
|
||||
private void SetupAudioQueue()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
AudioStreamBasicDescription format = AppleHardwareDeviceDriver.GetAudioFormat(
|
||||
RequestedSampleFormat,
|
||||
RequestedSampleRate,
|
||||
RequestedChannelCount);
|
||||
|
||||
nint callbackPtr = Marshal.GetFunctionPointerForDelegate(_callbackDelegate);
|
||||
nint userData = GCHandle.ToIntPtr(_gcHandle);
|
||||
|
||||
int result = AudioQueueNewOutput(
|
||||
ref format,
|
||||
callbackPtr,
|
||||
userData,
|
||||
nint.Zero,
|
||||
nint.Zero,
|
||||
0,
|
||||
out _audioQueue);
|
||||
|
||||
if (result != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"AudioQueueNewOutput failed: {result}");
|
||||
}
|
||||
|
||||
uint framesPerBuffer = RequestedSampleRate / 100;
|
||||
uint bufferSize = framesPerBuffer * (uint)_bytesPerFrame;
|
||||
|
||||
for (int i = 0; i < NumBuffers; i++)
|
||||
{
|
||||
AudioQueueAllocateBuffer(_audioQueue, bufferSize, out _audioQueueBuffers[i]);
|
||||
_bufferBytesFilled[i] = 0;
|
||||
|
||||
PrimeBuffer(_audioQueueBuffers[i], i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void PrimeBuffer(nint bufferPtr, int bufferIndex)
|
||||
{
|
||||
AudioQueueBuffer* buffer = (AudioQueueBuffer*)bufferPtr;
|
||||
|
||||
int capacityBytes = (int)buffer->AudioDataBytesCapacity;
|
||||
int framesPerBuffer = capacityBytes / _bytesPerFrame;
|
||||
|
||||
int availableFrames = _ringBuffer.Length / _bytesPerFrame;
|
||||
int framesToRead = Math.Min(availableFrames, framesPerBuffer);
|
||||
int bytesToRead = framesToRead * _bytesPerFrame;
|
||||
|
||||
Span<byte> dst = new((void*)buffer->AudioData, capacityBytes);
|
||||
dst.Clear();
|
||||
|
||||
if (bytesToRead > 0)
|
||||
{
|
||||
Span<byte> audio = dst.Slice(0, bytesToRead);
|
||||
_ringBuffer.Read(audio, 0, bytesToRead);
|
||||
ApplyVolume(buffer->AudioData, bytesToRead);
|
||||
}
|
||||
|
||||
buffer->AudioDataByteSize = (uint)capacityBytes;
|
||||
_bufferBytesFilled[bufferIndex] = bytesToRead;
|
||||
|
||||
AudioQueueEnqueueBuffer(_audioQueue, bufferPtr, 0, nint.Zero);
|
||||
}
|
||||
|
||||
private void OutputCallback(nint userData, nint audioQueue, nint bufferPtr)
|
||||
{
|
||||
if (!_started || bufferPtr == nint.Zero)
|
||||
return;
|
||||
|
||||
int bufferIndex = Array.IndexOf(_audioQueueBuffers, bufferPtr);
|
||||
if (bufferIndex < 0)
|
||||
return;
|
||||
|
||||
int bytesPlayed = _bufferBytesFilled[bufferIndex];
|
||||
if (bytesPlayed > 0)
|
||||
{
|
||||
ProcessPlayedSamples(bytesPlayed);
|
||||
}
|
||||
|
||||
PrimeBuffer(bufferPtr, bufferIndex);
|
||||
}
|
||||
|
||||
private void ProcessPlayedSamples(int bytesPlayed)
|
||||
{
|
||||
ulong samplesPlayed = GetSampleCount(bytesPlayed);
|
||||
ulong remaining = samplesPlayed;
|
||||
bool needUpdate = false;
|
||||
|
||||
while (remaining > 0 && _queuedBuffers.TryPeek(out AppleAudioBuffer buffer))
|
||||
{
|
||||
ulong needed = buffer.SampleCount - Interlocked.Read(ref buffer.SamplePlayed);
|
||||
ulong take = Math.Min(needed, remaining);
|
||||
|
||||
ulong played = Interlocked.Add(ref buffer.SamplePlayed, take);
|
||||
remaining -= take;
|
||||
|
||||
if (played == buffer.SampleCount)
|
||||
{
|
||||
_queuedBuffers.TryDequeue(out _);
|
||||
needUpdate = true;
|
||||
}
|
||||
|
||||
Interlocked.Add(ref _playedSampleCount, take);
|
||||
}
|
||||
|
||||
if (needUpdate)
|
||||
{
|
||||
_updateRequiredEvent.Set();
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void ApplyVolume(nint dataPtr, int byteSize)
|
||||
{
|
||||
float volume = Math.Clamp(_volume * _driver.Volume, 0f, 1f);
|
||||
if (volume >= 0.999f)
|
||||
return;
|
||||
|
||||
int sampleCount = byteSize / BackendHelper.GetSampleSize(RequestedSampleFormat);
|
||||
|
||||
switch (RequestedSampleFormat)
|
||||
{
|
||||
case SampleFormat.PcmInt16:
|
||||
short* s16 = (short*)dataPtr;
|
||||
for (int i = 0; i < sampleCount; i++)
|
||||
s16[i] = (short)(s16[i] * volume);
|
||||
break;
|
||||
|
||||
case SampleFormat.PcmFloat:
|
||||
float* f32 = (float*)dataPtr;
|
||||
for (int i = 0; i < sampleCount; i++)
|
||||
f32[i] *= volume;
|
||||
break;
|
||||
|
||||
case SampleFormat.PcmInt32:
|
||||
int* s32 = (int*)dataPtr;
|
||||
for (int i = 0; i < sampleCount; i++)
|
||||
s32[i] = (int)(s32[i] * volume);
|
||||
break;
|
||||
|
||||
case SampleFormat.PcmInt8:
|
||||
sbyte* s8 = (sbyte*)dataPtr;
|
||||
for (int i = 0; i < sampleCount; i++)
|
||||
s8[i] = (sbyte)(s8[i] * volume);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override void QueueBuffer(AudioBuffer buffer)
|
||||
{
|
||||
_ringBuffer.Write(buffer.Data, 0, buffer.Data.Length);
|
||||
_queuedBuffers.Enqueue(new AppleAudioBuffer(buffer.DataPointer, GetSampleCount(buffer)));
|
||||
}
|
||||
|
||||
public override void Start()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_started)
|
||||
return;
|
||||
|
||||
_started = true;
|
||||
AudioQueueStart(_audioQueue, nint.Zero);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Stop()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_started)
|
||||
return;
|
||||
|
||||
_started = false;
|
||||
AudioQueuePause(_audioQueue);
|
||||
}
|
||||
}
|
||||
|
||||
public override ulong GetPlayedSampleCount()
|
||||
=> Interlocked.Read(ref _playedSampleCount);
|
||||
|
||||
public override float GetVolume() => _volume;
|
||||
public override void SetVolume(float volume) => _volume = volume;
|
||||
|
||||
public override bool WasBufferFullyConsumed(AudioBuffer buffer)
|
||||
{
|
||||
if (!_queuedBuffers.TryPeek(out AppleAudioBuffer driverBuffer))
|
||||
return true;
|
||||
|
||||
return driverBuffer.DriverIdentifier != buffer.DataPointer;
|
||||
}
|
||||
|
||||
public override void PrepareToClose() { }
|
||||
public override void UnregisterBuffer(AudioBuffer buffer) { }
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
Stop();
|
||||
|
||||
if (_audioQueue != nint.Zero)
|
||||
{
|
||||
AudioQueueStop(_audioQueue, true);
|
||||
AudioQueueDispose(_audioQueue, true);
|
||||
_audioQueue = nint.Zero;
|
||||
}
|
||||
|
||||
if (_gcHandle.IsAllocated)
|
||||
{
|
||||
_gcHandle.Free();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
102
src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs
Normal file
102
src/Ryujinx.Audio.Backends.Apple/Native/AudioToolbox.cs
Normal file
@@ -0,0 +1,102 @@
|
||||
using System.Runtime.InteropServices;
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace Ryujinx.Audio.Backends.Apple.Native
|
||||
{
|
||||
public static partial class AudioToolbox
|
||||
{
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct AudioStreamBasicDescription
|
||||
{
|
||||
public double SampleRate;
|
||||
public uint FormatID;
|
||||
public uint FormatFlags;
|
||||
public uint BytesPerPacket;
|
||||
public uint FramesPerPacket;
|
||||
public uint BytesPerFrame;
|
||||
public uint ChannelsPerFrame;
|
||||
public uint BitsPerChannel;
|
||||
public uint Reserved;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct AudioChannelLayout
|
||||
{
|
||||
public uint AudioChannelLayoutTag;
|
||||
public uint AudioChannelBitmap;
|
||||
public uint NumberChannelDescriptions;
|
||||
}
|
||||
|
||||
internal const uint kAudioFormatLinearPCM = 0x6C70636D;
|
||||
internal const uint kAudioQueueProperty_ChannelLayout = 0x6171636c;
|
||||
internal const uint kAudioChannelLayoutTag_MPEG_5_1_A = 0x650006;
|
||||
internal const uint kAudioFormatFlagIsFloat = (1 << 0);
|
||||
internal const uint kAudioFormatFlagIsSignedInteger = (1 << 2);
|
||||
internal const uint kAudioFormatFlagIsPacked = (1 << 3);
|
||||
internal const uint kAudioFormatFlagIsBigEndian = (1 << 1);
|
||||
internal const uint kAudioFormatFlagIsAlignedHigh = (1 << 4);
|
||||
internal const uint kAudioFormatFlagIsNonInterleaved = (1 << 5);
|
||||
|
||||
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||
internal static partial int AudioQueueNewOutput(
|
||||
ref AudioStreamBasicDescription format,
|
||||
nint callback,
|
||||
nint userData,
|
||||
nint callbackRunLoop,
|
||||
nint callbackRunLoopMode,
|
||||
uint flags,
|
||||
out nint audioQueue);
|
||||
|
||||
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||
internal static partial int AudioQueueSetProperty(
|
||||
nint audioQueue,
|
||||
uint propertyID,
|
||||
ref AudioChannelLayout layout,
|
||||
uint layoutSize);
|
||||
|
||||
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||
internal static partial int AudioQueueDispose(nint audioQueue, [MarshalAs(UnmanagedType.I1)] bool immediate);
|
||||
|
||||
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||
internal static partial int AudioQueueAllocateBuffer(
|
||||
nint audioQueue,
|
||||
uint bufferByteSize,
|
||||
out nint buffer);
|
||||
|
||||
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||
internal static partial int AudioQueueStart(nint audioQueue, nint startTime);
|
||||
|
||||
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||
internal static partial int AudioQueuePause(nint audioQueue);
|
||||
|
||||
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||
internal static partial int AudioQueueStop(nint audioQueue, [MarshalAs(UnmanagedType.I1)] bool immediate);
|
||||
|
||||
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||
internal static partial int AudioQueueSetParameter(
|
||||
nint audioQueue,
|
||||
uint parameterID,
|
||||
float value);
|
||||
|
||||
[LibraryImport("/System/Library/Frameworks/AudioToolbox.framework/AudioToolbox")]
|
||||
internal static partial int AudioQueueEnqueueBuffer(
|
||||
nint audioQueue,
|
||||
nint buffer,
|
||||
uint numPacketDescs,
|
||||
nint packetDescs);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct AudioQueueBuffer
|
||||
{
|
||||
public uint AudioDataBytesCapacity;
|
||||
public nint AudioData;
|
||||
public uint AudioDataByteSize;
|
||||
public nint UserData;
|
||||
public uint PacketDescriptionCapacity;
|
||||
public nint PacketDescriptions;
|
||||
public uint PacketDescriptionCount;
|
||||
}
|
||||
|
||||
internal const uint kAudioQueueParam_Volume = 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -10,7 +10,8 @@ using static Ryujinx.Audio.Integration.IHardwareDeviceDriver;
|
||||
|
||||
namespace Ryujinx.Audio.Backends.OpenAL
|
||||
{
|
||||
public class OpenALHardwareDeviceDriver : IHardwareDeviceDriver
|
||||
// ReSharper disable once InconsistentNaming
|
||||
public sealed class OpenALHardwareDeviceDriver : IHardwareDeviceDriver
|
||||
{
|
||||
private readonly ALDevice _device;
|
||||
private readonly ALContext _context;
|
||||
@@ -148,7 +149,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
|
||||
@@ -9,7 +9,8 @@ using System.Threading;
|
||||
|
||||
namespace Ryujinx.Audio.Backends.OpenAL
|
||||
{
|
||||
class OpenALHardwareDeviceSession : HardwareDeviceSessionOutputBase
|
||||
// ReSharper disable once InconsistentNaming
|
||||
sealed class OpenALHardwareDeviceSession : HardwareDeviceSessionOutputBase
|
||||
{
|
||||
private readonly OpenALHardwareDeviceDriver _driver;
|
||||
private readonly int _sourceId;
|
||||
@@ -190,7 +191,7 @@ namespace Ryujinx.Audio.Backends.OpenAL
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && _driver.Unregister(this))
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Ryujinx.Audio.Backends.SDL3
|
||||
|
||||
using unsafe SDL_AudioStreamCallbackPointer = delegate* unmanaged[Cdecl]<nint, SDL_AudioStream*, int, int, void>;
|
||||
|
||||
public class SDL3HardwareDeviceDriver : IHardwareDeviceDriver
|
||||
public sealed class SDL3HardwareDeviceDriver : IHardwareDeviceDriver
|
||||
{
|
||||
private readonly ManualResetEvent _updateRequiredEvent;
|
||||
private readonly ManualResetEvent _pauseEvent;
|
||||
@@ -162,7 +162,7 @@ namespace Ryujinx.Audio.Backends.SDL3
|
||||
Dispose(true);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
|
||||
@@ -12,10 +12,7 @@ using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ryujinx.Audio.Backends.SDL3
|
||||
{
|
||||
|
||||
|
||||
|
||||
unsafe class SDL3HardwareDeviceSession : HardwareDeviceSessionOutputBase
|
||||
sealed unsafe class SDL3HardwareDeviceSession : HardwareDeviceSessionOutputBase
|
||||
{
|
||||
private readonly SDL3HardwareDeviceDriver _driver;
|
||||
private readonly ConcurrentQueue<SDL3AudioBuffer> _queuedBuffers;
|
||||
@@ -226,7 +223,7 @@ namespace Ryujinx.Audio.Backends.SDL3
|
||||
return driverBuffer.DriverIdentifier != buffer.DataPointer;
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && _driver.Unregister(this))
|
||||
{
|
||||
|
||||
@@ -130,7 +130,7 @@ namespace Ryujinx.Audio.Backends.SoundIo.Native
|
||||
unsafe
|
||||
{
|
||||
int* frameCountPtr = &nativeFrameCount;
|
||||
IntPtr* arenasPtr = &arenas;
|
||||
nint* arenasPtr = &arenas;
|
||||
CheckError(soundio_outstream_begin_write(_context, (nint)arenasPtr, (nint)frameCountPtr));
|
||||
|
||||
frameCount = *frameCountPtr;
|
||||
|
||||
@@ -10,7 +10,7 @@ using static Ryujinx.Audio.Integration.IHardwareDeviceDriver;
|
||||
|
||||
namespace Ryujinx.Audio.Backends.SoundIo
|
||||
{
|
||||
public class SoundIoHardwareDeviceDriver : IHardwareDeviceDriver
|
||||
public sealed class SoundIoHardwareDeviceDriver : IHardwareDeviceDriver
|
||||
{
|
||||
private readonly SoundIoContext _audioContext;
|
||||
private readonly SoundIoDeviceContext _audioDevice;
|
||||
@@ -227,7 +227,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ using static Ryujinx.Audio.Backends.SoundIo.Native.SoundIo;
|
||||
|
||||
namespace Ryujinx.Audio.Backends.SoundIo
|
||||
{
|
||||
class SoundIoHardwareDeviceSession : HardwareDeviceSessionOutputBase
|
||||
sealed class SoundIoHardwareDeviceSession : HardwareDeviceSessionOutputBase
|
||||
{
|
||||
private readonly SoundIoHardwareDeviceDriver _driver;
|
||||
private readonly ConcurrentQueue<SoundIoAudioBuffer> _queuedBuffers;
|
||||
@@ -428,7 +428,7 @@ namespace Ryujinx.Audio.Backends.SoundIo
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
private void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing && _driver.Unregister(this))
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Ryujinx.Audio.Renderer.Common
|
||||
public uint MixesSize;
|
||||
public uint SinksSize;
|
||||
public uint PerformanceBufferSize;
|
||||
public uint Unknown24;
|
||||
public uint SplitterSize;
|
||||
public uint RenderInfoSize;
|
||||
|
||||
#pragma warning disable IDE0051, CS0169 // Remove unused field
|
||||
|
||||
@@ -433,8 +433,12 @@ namespace Ryujinx.Audio.Renderer.Server
|
||||
|
||||
public ResultCode UpdateSplitter(SplitterContext context)
|
||||
{
|
||||
long initialInputConsumed = _inputReader.Consumed;
|
||||
|
||||
if (context.Update(ref _inputReader))
|
||||
{
|
||||
_inputReader.SetConsumed(initialInputConsumed + _inputHeader.SplitterSize);
|
||||
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,9 +11,7 @@ namespace Ryujinx.BuildValidationTasks
|
||||
{
|
||||
static readonly JsonSerializerOptions _jsonOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
NewLine = "\n",
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
WriteIndented = true, NewLine = "\n", Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
public LocalesValidationTask() { }
|
||||
@@ -22,77 +20,116 @@ namespace Ryujinx.BuildValidationTasks
|
||||
{
|
||||
Console.WriteLine("Running Locale Validation Task...");
|
||||
|
||||
string path = projectPath + "assets/locales.json";
|
||||
bool encounteredIssue = false;
|
||||
string langPath = projectPath + "assets/Languages.json";
|
||||
string data;
|
||||
|
||||
using (StreamReader sr = new(path))
|
||||
using (StreamReader sr = new(langPath))
|
||||
{
|
||||
data = sr.ReadToEnd();
|
||||
}
|
||||
|
||||
LocalesJson json;
|
||||
|
||||
if (isGitRunner && data.Contains("\r\n"))
|
||||
throw new FormatException("locales.json is using CRLF line endings! It should be using LF line endings, rebuild locally to fix...");
|
||||
throw new FormatException("Languages.json is using CRLF line endings! It should be using LF line endings, rebuild locally to fix...");
|
||||
|
||||
LanguagesJson langJson;
|
||||
|
||||
try
|
||||
{
|
||||
json = JsonSerializer.Deserialize<LocalesJson>(data);
|
||||
|
||||
langJson = JsonSerializer.Deserialize<LanguagesJson>(data);
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
throw new JsonException(e.Message); //shorter and easier stacktrace
|
||||
}
|
||||
|
||||
bool encounteredIssue = false;
|
||||
|
||||
for (int i = 0; i < json.Locales.Count; i++)
|
||||
foreach ((string code, string lang) in langJson.Languages)
|
||||
{
|
||||
LocalesEntry locale = json.Locales[i];
|
||||
|
||||
foreach (string langCode in json.Languages.Where(lang => !locale.Translations.ContainsKey(lang)))
|
||||
if (string.IsNullOrEmpty(lang))
|
||||
{
|
||||
encounteredIssue = true;
|
||||
|
||||
if (!isGitRunner)
|
||||
{
|
||||
locale.Translations.Add(langCode, string.Empty);
|
||||
Console.WriteLine($"Added '{langCode}' to Locale '{locale.ID}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Missing '{langCode}' in Locale '{locale.ID}'!");
|
||||
}
|
||||
throw new JsonException($"{code} language name missing!");
|
||||
}
|
||||
|
||||
foreach (string langCode in json.Languages.Where(lang => locale.Translations.ContainsKey(lang) && lang != "en_US" && locale.Translations[lang] == locale.Translations["en_US"]))
|
||||
{
|
||||
encounteredIssue = true;
|
||||
|
||||
if (!isGitRunner)
|
||||
{
|
||||
locale.Translations[langCode] = string.Empty;
|
||||
Console.WriteLine($"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'! Resetting it...");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'!");
|
||||
}
|
||||
}
|
||||
|
||||
locale.Translations = locale.Translations.OrderBy(pair => pair.Key).ToDictionary(pair => pair.Key, pair => pair.Value);
|
||||
json.Locales[i] = locale;
|
||||
}
|
||||
|
||||
if (isGitRunner && encounteredIssue)
|
||||
throw new JsonException("1 or more locales are invalid! Rebuild locally to fix...");
|
||||
string folderPath = projectPath + "assets/Locales/";
|
||||
|
||||
string jsonString = JsonSerializer.Serialize(json, _jsonOptions);
|
||||
string[] paths = Directory.GetFiles(folderPath, "*.json", SearchOption.AllDirectories);
|
||||
|
||||
using (StreamWriter sw = new(path))
|
||||
foreach (string path in paths)
|
||||
{
|
||||
sw.Write(jsonString);
|
||||
using (StreamReader sr = new(path))
|
||||
{
|
||||
data = sr.ReadToEnd();
|
||||
}
|
||||
|
||||
if (isGitRunner && data.Contains("\r\n"))
|
||||
throw new FormatException($"{Path.GetFileName(path)} is using CRLF line endings! It should be using LF line endings, rebuild locally to fix...");
|
||||
|
||||
LocalesJson json;
|
||||
|
||||
try
|
||||
{
|
||||
json = JsonSerializer.Deserialize<LocalesJson>(data);
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
throw new JsonException(e.Message); //shorter and easier stacktrace
|
||||
}
|
||||
|
||||
|
||||
for (int i = 0; i < json.Locales.Count; i++)
|
||||
{
|
||||
LocalesEntry locale = json.Locales[i];
|
||||
|
||||
foreach (string langCode in
|
||||
langJson.Languages.Keys.Where(lang => !locale.Translations.ContainsKey(lang)))
|
||||
{
|
||||
encounteredIssue = true;
|
||||
|
||||
if (!isGitRunner)
|
||||
{
|
||||
locale.Translations.Add(langCode, string.Empty);
|
||||
Console.WriteLine($"Added '{langCode}' to Locale '{locale.ID}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"Missing '{langCode}' in Locale '{locale.ID}'!");
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string langCode in langJson.Languages.Keys.Where(lang =>
|
||||
locale.Translations.ContainsKey(lang) && lang != "en_US" &&
|
||||
locale.Translations[lang] == locale.Translations["en_US"]))
|
||||
{
|
||||
encounteredIssue = true;
|
||||
|
||||
if (!isGitRunner)
|
||||
{
|
||||
locale.Translations[langCode] = string.Empty;
|
||||
Console.WriteLine(
|
||||
$"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'! Resetting it...");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"Lanugage '{langCode}' is a duplicate of en_US in Locale '{locale.ID}'!");
|
||||
}
|
||||
}
|
||||
|
||||
locale.Translations = locale.Translations.OrderBy(pair => pair.Key)
|
||||
.ToDictionary(pair => pair.Key, pair => pair.Value);
|
||||
json.Locales[i] = locale;
|
||||
}
|
||||
|
||||
if (isGitRunner && encounteredIssue)
|
||||
throw new JsonException("1 or more locales are invalid! Rebuild locally to fix...");
|
||||
|
||||
string jsonString = JsonSerializer.Serialize(json, _jsonOptions);
|
||||
|
||||
using (StreamWriter sw = new(path))
|
||||
{
|
||||
sw.Write(jsonString);
|
||||
}
|
||||
}
|
||||
|
||||
Console.WriteLine("Finished Locale Validation Task!");
|
||||
@@ -100,10 +137,13 @@ namespace Ryujinx.BuildValidationTasks
|
||||
return true;
|
||||
}
|
||||
|
||||
struct LanguagesJson
|
||||
{
|
||||
public Dictionary<string, string> Languages { get; set; }
|
||||
}
|
||||
|
||||
struct LocalesJson
|
||||
{
|
||||
public Dictionary<string, string> Info { get; set; }
|
||||
public List<string> Languages { get; set; }
|
||||
public List<LocalesEntry> Locales { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ namespace Ryujinx.Common
|
||||
public static string[] GetAllAvailableResources(string path, string ext = "")
|
||||
{
|
||||
return ResolveManifestPath(path).Item1.GetManifestResourceNames()
|
||||
.Where(r => r.EndsWith(ext))
|
||||
.Where(r => r.StartsWith(path.Replace('/', '.')) && r.EndsWith(ext))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ namespace ARMeilleure.Common
|
||||
/// <summary>
|
||||
/// Base address for the page.
|
||||
/// </summary>
|
||||
public readonly IntPtr Address;
|
||||
public readonly nint Address;
|
||||
|
||||
public AddressTablePage(bool isSparse, IntPtr address)
|
||||
public AddressTablePage(bool isSparse, nint address)
|
||||
{
|
||||
IsSparse = isSparse;
|
||||
Address = address;
|
||||
@@ -47,20 +47,20 @@ namespace ARMeilleure.Common
|
||||
public readonly SparseMemoryBlock Block;
|
||||
private readonly TrackingEventDelegate _trackingEvent;
|
||||
|
||||
public TableSparseBlock(ulong size, Action<IntPtr> ensureMapped, PageInitDelegate pageInit)
|
||||
public TableSparseBlock(ulong size, Action<nint> ensureMapped, PageInitDelegate pageInit)
|
||||
{
|
||||
SparseMemoryBlock block = new(size, pageInit, null);
|
||||
|
||||
_trackingEvent = (address, size, write) =>
|
||||
{
|
||||
ulong pointer = (ulong)block.Block.Pointer + address;
|
||||
ensureMapped((IntPtr)pointer);
|
||||
ensureMapped((nint)pointer);
|
||||
return pointer;
|
||||
};
|
||||
|
||||
bool added = NativeSignalHandler.AddTrackedRegion(
|
||||
(nuint)block.Block.Pointer,
|
||||
(nuint)(block.Block.Pointer + (IntPtr)block.Block.Size),
|
||||
(nuint)(block.Block.Pointer + (nint)block.Block.Size),
|
||||
Marshal.GetFunctionPointerForDelegate(_trackingEvent));
|
||||
|
||||
if (!added)
|
||||
@@ -116,7 +116,7 @@ namespace ARMeilleure.Common
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IntPtr Base
|
||||
public nint Base
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -124,7 +124,7 @@ namespace ARMeilleure.Common
|
||||
|
||||
lock (_pages)
|
||||
{
|
||||
return (IntPtr)GetRootPage();
|
||||
return (nint)GetRootPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,7 +240,7 @@ namespace ARMeilleure.Common
|
||||
|
||||
long index = Levels[^1].GetValue(address);
|
||||
|
||||
EnsureMapped((IntPtr)(page + index));
|
||||
EnsureMapped((nint)(page + index));
|
||||
|
||||
return ref page[index];
|
||||
}
|
||||
@@ -284,7 +284,7 @@ namespace ARMeilleure.Common
|
||||
/// Ensure the given pointer is mapped in any overlapping sparse reservations.
|
||||
/// </summary>
|
||||
/// <param name="ptr">Pointer to be mapped</param>
|
||||
private void EnsureMapped(IntPtr ptr)
|
||||
private void EnsureMapped(nint ptr)
|
||||
{
|
||||
if (Sparse)
|
||||
{
|
||||
@@ -299,7 +299,7 @@ namespace ARMeilleure.Common
|
||||
{
|
||||
SparseMemoryBlock sparse = reserved.Block;
|
||||
|
||||
if (ptr >= sparse.Block.Pointer && ptr < sparse.Block.Pointer + (IntPtr)sparse.Block.Size)
|
||||
if (ptr >= sparse.Block.Pointer && ptr < sparse.Block.Pointer + (nint)sparse.Block.Size)
|
||||
{
|
||||
sparse.EnsureMapped((ulong)(ptr - sparse.Block.Pointer));
|
||||
|
||||
@@ -319,15 +319,15 @@ namespace ARMeilleure.Common
|
||||
/// </summary>
|
||||
/// <param name="level">Level to get the fill value for</param>
|
||||
/// <returns>The fill value</returns>
|
||||
private IntPtr GetFillValue(int level)
|
||||
private nint GetFillValue(int level)
|
||||
{
|
||||
if (_fillBottomLevel != null && level == Levels.Length - 2)
|
||||
{
|
||||
return (IntPtr)_fillBottomLevelPtr;
|
||||
return (nint)_fillBottomLevelPtr;
|
||||
}
|
||||
else
|
||||
{
|
||||
return IntPtr.Zero;
|
||||
return nint.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,7 +379,7 @@ namespace ARMeilleure.Common
|
||||
/// <param name="fill">Fill value</param>
|
||||
/// <param name="leaf"><see langword="true"/> if leaf; otherwise <see langword="false"/></param>
|
||||
/// <returns>Allocated block</returns>
|
||||
private IntPtr Allocate<T>(int length, T fill, bool leaf) where T : unmanaged
|
||||
private nint Allocate<T>(int length, T fill, bool leaf) where T : unmanaged
|
||||
{
|
||||
int size = sizeof(T) * length;
|
||||
|
||||
@@ -405,7 +405,7 @@ namespace ARMeilleure.Common
|
||||
}
|
||||
}
|
||||
|
||||
page = new AddressTablePage(true, block.Block.Pointer + (IntPtr)_sparseReservedOffset);
|
||||
page = new AddressTablePage(true, block.Block.Pointer + (nint)_sparseReservedOffset);
|
||||
|
||||
_sparseReservedOffset += (ulong)size;
|
||||
|
||||
@@ -413,7 +413,7 @@ namespace ARMeilleure.Common
|
||||
}
|
||||
else
|
||||
{
|
||||
IntPtr address = (IntPtr)NativeAllocator.Instance.Allocate((uint)size);
|
||||
nint address = (nint)NativeAllocator.Instance.Allocate((uint)size);
|
||||
page = new AddressTablePage(false, address);
|
||||
|
||||
Span<T> span = new((void*)page.Address, length);
|
||||
|
||||
@@ -658,7 +658,7 @@ namespace Ryujinx.Graphics.Gpu.Image
|
||||
|
||||
bool canImport = Storage.Info.IsLinear && Storage.Info.Stride >= Storage.Info.Width * Storage.Info.FormatInfo.BytesPerPixel;
|
||||
|
||||
IntPtr hostPointer = canImport ? _physicalMemory.GetHostPointer(Storage.Range) : 0;
|
||||
nint hostPointer = canImport ? _physicalMemory.GetHostPointer(Storage.Range) : 0;
|
||||
|
||||
if (hostPointer != 0 && _context.Renderer.PrepareHostMapping(hostPointer, Storage.Size))
|
||||
{
|
||||
|
||||
12
src/Ryujinx.Graphics.RenderDocApi/Capture.cs
Normal file
12
src/Ryujinx.Graphics.RenderDocApi/Capture.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.RenderDocApi
|
||||
{
|
||||
public readonly record struct Capture(int Index, string FileName, DateTime Timestamp)
|
||||
{
|
||||
public void SetComments(string comments)
|
||||
{
|
||||
RenderDoc.SetCaptureFileComments(FileName, comments);
|
||||
}
|
||||
}
|
||||
}
|
||||
100
src/Ryujinx.Graphics.RenderDocApi/CaptureOption.cs
Normal file
100
src/Ryujinx.Graphics.RenderDocApi/CaptureOption.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
namespace Ryujinx.Graphics.RenderDocApi
|
||||
{
|
||||
public enum CaptureOption
|
||||
{
|
||||
/// <summary>
|
||||
/// specifies whether the application is allowed to enable vsync. Default is on.
|
||||
/// </summary>
|
||||
AllowVsync = 0,
|
||||
/// <summary>
|
||||
/// specifies whether the application is allowed to enter exclusive fullscreen. Default is on.
|
||||
/// </summary>
|
||||
AllowFullscreen = 1,
|
||||
/// <summary>
|
||||
/// specifies whether (where possible) API-specific debugging is enabled. Default is off.
|
||||
/// </summary>
|
||||
ApiValidation = 2,
|
||||
/// <summary>
|
||||
/// specifies whether each API call should save a callstack. Default is off.
|
||||
/// </summary>
|
||||
CaptureCallstacks = 3,
|
||||
/// <summary>
|
||||
/// specifies whether, if <see cref="CaptureCallstacks"/> is enabled, callstacks are only saved on actions. Default is off.
|
||||
/// </summary>
|
||||
CaptureCallstacksOnlyDraws = 4,
|
||||
/// <summary>
|
||||
/// specifies a delay in seconds after launching a process to pause, to allow debuggers to attach. <br/>
|
||||
/// This will only apply to child processes since the delay happens at process startup. Default is 0.
|
||||
/// </summary>
|
||||
DelayForDebugger = 5,
|
||||
/// <summary>
|
||||
/// specifies whether any mapped memory updates should be bounds-checked for overruns,
|
||||
/// and uninitialised buffers are initialized to <code>0xDDDDDDDD</code> to catch use of uninitialised data.
|
||||
/// Only supported on D3D11 and OpenGL. Default is off.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This option is only valid for OpenGL and D3D11. Explicit APIs such as D3D12 and Vulkan do
|
||||
/// not do the same kind of interception & checking, and undefined contents are really undefined.
|
||||
/// </remarks>
|
||||
VerifyBufferAccess = 6,
|
||||
/// <summary>
|
||||
/// Hooks any system API calls that create child processes, and injects
|
||||
/// RenderDoc into them recursively with the same options.
|
||||
/// </summary>
|
||||
HookIntoChildren = 7,
|
||||
/// <summary>
|
||||
/// specifies whether all live resources at the time of capture should be included in the capture,
|
||||
/// even if they are not referenced by the frame. Default is off.
|
||||
/// </summary>
|
||||
RefAllSources = 8,
|
||||
/// <summary>
|
||||
/// By default, RenderDoc skips saving initial states for resources where the
|
||||
/// previous contents don't appear to be used, assuming that writes before
|
||||
/// reads indicate previous contents aren't used.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// **NOTE**: As of RenderDoc v1.1 this option has been deprecated. Setting or
|
||||
/// getting it will be ignored, to allow compatibility with older versions.
|
||||
/// In v1.1 the option acts as if it's always enabled.
|
||||
/// </remarks>
|
||||
SaveAllInitials = 9,
|
||||
/// <summary>
|
||||
/// In APIs that allow for the recording of command lists to be replayed later,
|
||||
/// RenderDoc may choose to not capture command lists before a frame capture is
|
||||
/// triggered, to reduce overheads. This means any command lists recorded once
|
||||
/// and replayed many times will not be available and may cause a failure to
|
||||
/// capture.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// NOTE: This is only true for APIs where multithreading is difficult or
|
||||
/// discouraged. Newer APIs like Vulkan and D3D12 will ignore this option
|
||||
/// and always capture all command lists since the API is heavily oriented
|
||||
/// around it and the overheads have been reduced by API design.
|
||||
/// </remarks>
|
||||
CaptureAllCmdLists = 10,
|
||||
/// <summary>
|
||||
/// Mute API debugging output when the <see cref="ApiValidation"/> option is enabled.
|
||||
/// </summary>
|
||||
DebugOutputMute = 11,
|
||||
/// <summary>
|
||||
/// Allow vendor extensions to be used even when they may be
|
||||
/// incompatible with RenderDoc and cause corrupted replays or crashes.
|
||||
/// </summary>
|
||||
AllowUnsupportedVendorExtensions = 12,
|
||||
/// <summary>
|
||||
/// Define a soft memory limit which some APIs may aim to keep overhead under where
|
||||
/// possible. Anything above this limit will where possible be saved directly to disk during
|
||||
/// capture.<br/>
|
||||
/// This will cause increased disk space use (which may cause a capture to fail if disk space is
|
||||
/// exhausted) as well as slower capture times.
|
||||
/// <br/><br/>
|
||||
/// Not all memory allocations may be deferred like this so it is not a guarantee of a memory
|
||||
/// limit.
|
||||
/// <br/><br/>
|
||||
/// Units are in MBs, suggested values would range from 200MB to 1000MB.
|
||||
/// </summary>
|
||||
SoftMemoryLimit = 13,
|
||||
}
|
||||
}
|
||||
83
src/Ryujinx.Graphics.RenderDocApi/InputButton.cs
Normal file
83
src/Ryujinx.Graphics.RenderDocApi/InputButton.cs
Normal file
@@ -0,0 +1,83 @@
|
||||
|
||||
// ReSharper disable UnusedMember.Global
|
||||
namespace Ryujinx.Graphics.RenderDocApi
|
||||
{
|
||||
public enum InputButton
|
||||
{
|
||||
// '0' - '9' matches ASCII values
|
||||
Key0 = 0x30,
|
||||
Key1 = 0x31,
|
||||
Key2 = 0x32,
|
||||
Key3 = 0x33,
|
||||
Key4 = 0x34,
|
||||
Key5 = 0x35,
|
||||
Key6 = 0x36,
|
||||
Key7 = 0x37,
|
||||
Key8 = 0x38,
|
||||
Key9 = 0x39,
|
||||
|
||||
// 'A' - 'Z' matches ASCII values
|
||||
A = 0x41,
|
||||
B = 0x42,
|
||||
C = 0x43,
|
||||
D = 0x44,
|
||||
E = 0x45,
|
||||
F = 0x46,
|
||||
G = 0x47,
|
||||
H = 0x48,
|
||||
I = 0x49,
|
||||
J = 0x4A,
|
||||
K = 0x4B,
|
||||
L = 0x4C,
|
||||
M = 0x4D,
|
||||
N = 0x4E,
|
||||
O = 0x4F,
|
||||
P = 0x50,
|
||||
Q = 0x51,
|
||||
R = 0x52,
|
||||
S = 0x53,
|
||||
T = 0x54,
|
||||
U = 0x55,
|
||||
V = 0x56,
|
||||
W = 0x57,
|
||||
X = 0x58,
|
||||
Y = 0x59,
|
||||
Z = 0x5A,
|
||||
|
||||
// leave the rest of the ASCII range free
|
||||
// in case we want to use it later
|
||||
NonPrintable = 0x100,
|
||||
|
||||
Divide,
|
||||
Multiply,
|
||||
Subtract,
|
||||
Plus,
|
||||
|
||||
F1,
|
||||
F2,
|
||||
F3,
|
||||
F4,
|
||||
F5,
|
||||
F6,
|
||||
F7,
|
||||
F8,
|
||||
F9,
|
||||
F10,
|
||||
F11,
|
||||
F12,
|
||||
|
||||
Home,
|
||||
End,
|
||||
Insert,
|
||||
Delete,
|
||||
PageUp,
|
||||
PageDn,
|
||||
|
||||
Backspace,
|
||||
Tab,
|
||||
PrtScrn,
|
||||
Pause,
|
||||
|
||||
Max,
|
||||
}
|
||||
}
|
||||
39
src/Ryujinx.Graphics.RenderDocApi/OverlayBits.cs
Normal file
39
src/Ryujinx.Graphics.RenderDocApi/OverlayBits.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
// ReSharper disable UnusedMember.Global
|
||||
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.RenderDocApi
|
||||
{
|
||||
[Flags]
|
||||
public enum OverlayBits
|
||||
{
|
||||
/// <summary>
|
||||
/// This single bit controls whether the overlay is enabled or disabled globally
|
||||
/// </summary>
|
||||
Enabled = 1 << 0,
|
||||
/// <summary>
|
||||
/// Show the average framerate over several seconds as well as min/max
|
||||
/// </summary>
|
||||
FrameRate = 1 << 1,
|
||||
/// <summary>
|
||||
/// Show the current frame number
|
||||
/// </summary>
|
||||
FrameNumber = 1 << 2,
|
||||
/// <summary>
|
||||
/// Show a list of recent captures, and how many captures have been made
|
||||
/// </summary>
|
||||
CaptureList = 1 << 3,
|
||||
/// <summary>
|
||||
/// Default values for the overlay mask
|
||||
/// </summary>
|
||||
Default = Enabled | FrameRate | FrameNumber | CaptureList,
|
||||
/// <summary>
|
||||
/// Enable all bits
|
||||
/// </summary>
|
||||
All = ~0,
|
||||
/// <summary>
|
||||
/// Disable all bits
|
||||
/// </summary>
|
||||
None = 0
|
||||
}
|
||||
}
|
||||
5
src/Ryujinx.Graphics.RenderDocApi/README.md
Normal file
5
src/Ryujinx.Graphics.RenderDocApi/README.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Ryujinx.Graphics.RenderDocApi
|
||||
|
||||
This is a C# binding for RenderDoc's application API.
|
||||
This is a source-inclusion of https://github.com/utkumaden/RenderdocSharp.
|
||||
I didn't use the NuGet package as I had a few minor changes I wanted to make, and I want to learn from it as well via hands-on experience.
|
||||
639
src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs
Normal file
639
src/Ryujinx.Graphics.RenderDocApi/RenderDoc.cs
Normal file
@@ -0,0 +1,639 @@
|
||||
using System;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Ryujinx.Graphics.RenderDocApi
|
||||
{
|
||||
public static unsafe partial class RenderDoc
|
||||
{
|
||||
/// <summary>
|
||||
/// True if the API is available.
|
||||
/// </summary>
|
||||
public static bool IsAvailable => Api != null;
|
||||
|
||||
/// <summary>
|
||||
/// Set the minimum version of the API you require.
|
||||
/// </summary>
|
||||
/// <remarks>Set this before you do anything else with the RenderDoc API, including <see cref="IsAvailable"/>.</remarks>
|
||||
public static RenderDocVersion MinimumRequired { get; set; } = RenderDocVersion.Version_1_0_0;
|
||||
|
||||
/// <summary>
|
||||
/// Set to true to assert versions.
|
||||
/// </summary>
|
||||
public static bool AssertVersionEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Version of the API available.
|
||||
/// </summary>
|
||||
[MemberNotNullWhen(true, nameof(IsAvailable))]
|
||||
public static Version? Version
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsAvailable)
|
||||
return null;
|
||||
|
||||
int major, minor, build;
|
||||
Api->GetApiVersion(&major, &minor, &build);
|
||||
return new Version(major, minor, build);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The current mask which determines what sections of the overlay render on each window.
|
||||
/// </summary>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static OverlayBits OverlayBits
|
||||
{
|
||||
get => Api->GetOverlayBits();
|
||||
set
|
||||
{
|
||||
Api->MaskOverlayBits(~value, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The template for new captures.<br/>
|
||||
/// The template can either be a relative or absolute path, which determines where captures will be saved and how they will be named.
|
||||
/// If the path template is 'my_captures/example', then captures saved will be e.g.
|
||||
/// 'my_captures/example_frame123.rdc' and 'my_captures/example_frame456.rdc'.<br/>
|
||||
/// Relative paths will be saved relative to the process’s current working directory.<br/>
|
||||
/// </summary>
|
||||
/// <remarks>The default template is in a folder controlled by the UI - initially the system temporary folder, and the filename is the executable’s filename.</remarks>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static string CaptureFilePathTemplate
|
||||
{
|
||||
get
|
||||
{
|
||||
byte* ptr = Api->GetCaptureFilePathTemplate();
|
||||
return Marshal.PtrToStringUTF8((nint)ptr)!;
|
||||
}
|
||||
set
|
||||
{
|
||||
fixed (byte* ptr = value.ToNullTerminatedByteArray())
|
||||
{
|
||||
Api->SetCaptureFilePathTemplate(ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The amount of frame captures that have been made.
|
||||
/// </summary>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static int CaptureCount => Api->GetNumCaptures();
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the RenderDoc UI is currently connected to this process.
|
||||
/// </summary>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static bool IsTargetControlConnected => Api is not null && Api->IsTargetControlConnected() != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the current frame is capturing.
|
||||
/// </summary>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static bool IsFrameCapturing => Api is not null && Api->IsFrameCapturing() != 0;
|
||||
|
||||
/// <summary>
|
||||
/// Set one of the options for tweaking some behaviors of capturing.
|
||||
/// </summary>
|
||||
/// <param name="option">specifies which capture option should be set.</param>
|
||||
/// <param name="integer">the unsigned integer value to set for the option.</param>
|
||||
/// <remarks>Note that each option only takes effect from after it is set - so it is advised to set these options as early as possible, ideally before any graphics API has been initialized.</remarks>
|
||||
/// <returns>
|
||||
/// true, if the <paramref name="option"/> is valid, and the value set on the option is within valid ranges.<br/>
|
||||
/// false, if the option is not a <see cref="CaptureOption"/>, or the value is not valid for the option.
|
||||
/// </returns>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static bool SetCaptureOption(CaptureOption option, uint integer)
|
||||
{
|
||||
return Api is not null && Api->SetCaptureOptionU32(option, integer) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set one of the options for tweaking some behaviors of capturing.
|
||||
/// </summary>
|
||||
/// <param name="option">specifies which capture option should be set.</param>
|
||||
/// <param name="boolean">the value to set for the option, converted to a 0 or 1 before setting.</param>
|
||||
/// <remarks>Note that each option only takes effect from after it is set - so it is advised to set these options as early as possible, ideally before any graphics API has been initialized.</remarks>
|
||||
/// <returns>
|
||||
/// true, if the <paramref name="option"/> is valid, and the value set on the option is within valid ranges.<br/>
|
||||
/// false, if the option is not a <see cref="CaptureOption"/>, or the value is not valid for the option.
|
||||
/// </returns>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static bool SetCaptureOption(CaptureOption option, bool boolean)
|
||||
=> SetCaptureOption(option, boolean ? 1 : 0);
|
||||
|
||||
/// <summary>
|
||||
/// Set one of the options for tweaking some behaviors of capturing.
|
||||
/// </summary>
|
||||
/// <param name="option">specifies which capture option should be set.</param>
|
||||
/// <param name="single">the floating point value to set for the option.</param>
|
||||
/// <remarks>Note that each option only takes effect from after it is set - so it is advised to set these options as early as possible, ideally before any graphics API has been initialized.</remarks>
|
||||
/// <returns>
|
||||
/// true, if the <paramref name="option"/> is valid, and the value set on the option is within valid ranges.<br/>
|
||||
/// false, if the option is not a <see cref="CaptureOption"/>, or the value is not valid for the option.
|
||||
/// </returns>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static bool SetCaptureOption(CaptureOption option, float single)
|
||||
{
|
||||
return Api is not null && Api->SetCaptureOptionF32(option, single) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>, writing it to an out parameter.
|
||||
/// </summary>
|
||||
/// <param name="option">specifies which capture option should be retrieved.</param>
|
||||
/// <param name="integer">the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum. Otherwise, <see cref="int.MaxValue"/>.</param>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static void GetCaptureOption(CaptureOption option, out uint integer)
|
||||
{
|
||||
integer = Api->GetCaptureOptionU32(option);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>, writing it to an out parameter.
|
||||
/// </summary>
|
||||
/// <param name="option">specifies which capture option should be retrieved.</param>
|
||||
/// <param name="single">the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum. Otherwise, -<see cref="float.MaxValue"/>.</param>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static void GetCaptureOption(CaptureOption option, out float single)
|
||||
{
|
||||
single = Api->GetCaptureOptionF32(option);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>,
|
||||
/// converted to a boolean.
|
||||
/// </summary>
|
||||
/// <param name="option">specifies which capture option should be retrieved.</param>
|
||||
/// <returns>
|
||||
/// the value of the capture option, converted to bool, if the option is a valid <see cref="CaptureOption"/> enum.
|
||||
/// Otherwise, returns null.
|
||||
/// </returns>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static bool? GetCaptureOptionBool(CaptureOption option)
|
||||
{
|
||||
if (Api is null) return false;
|
||||
|
||||
uint returnVal = GetCaptureOptionU32(option);
|
||||
if (returnVal == uint.MaxValue)
|
||||
return null;
|
||||
|
||||
return returnVal is not 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>.
|
||||
/// </summary>
|
||||
/// <param name="option">specifies which capture option should be retrieved.</param>
|
||||
/// <returns>
|
||||
/// the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum.
|
||||
/// Otherwise, returns <see cref="int.MaxValue"/>.
|
||||
/// </returns>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static uint GetCaptureOptionU32(CaptureOption option) => Api->GetCaptureOptionU32(option);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current value of one of the different options in <see cref="CaptureOption"/>.
|
||||
/// </summary>
|
||||
/// <param name="option">specifies which capture option should be retrieved.</param>
|
||||
/// <returns>
|
||||
/// the value of the capture option, if the option is a valid <see cref="CaptureOption"/> enum.
|
||||
/// Otherwise, returns -<see cref="float.MaxValue"/>.
|
||||
/// </returns>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static float GetCaptureOptionF32(CaptureOption option) => Api->GetCaptureOptionF32(option);
|
||||
|
||||
/// <summary>
|
||||
/// Changes the key bindings in-application for changing the focussed window.
|
||||
/// </summary>
|
||||
/// <param name="buttons">lists the keys to bind.</param>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static void SetFocusToggleKeys(ReadOnlySpan<InputButton> buttons)
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
fixed (InputButton* ptr = buttons)
|
||||
{
|
||||
Api->SetFocusToggleKeys(ptr, buttons.Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the key bindings in-application for triggering a capture on the current window.
|
||||
/// </summary>
|
||||
/// <param name="buttons">lists the keys to bind.</param>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static void SetCaptureKeys(ReadOnlySpan<InputButton> buttons)
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
fixed (InputButton* ptr = buttons)
|
||||
{
|
||||
Api->SetCaptureKeys(ptr, buttons.Length);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to remove RenderDoc and its hooks from the target process.<br/>
|
||||
/// It must be called as early as possible in the process, and will have undefined results
|
||||
/// if any graphics API functions have been called.
|
||||
/// </summary>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static void RemoveHooks()
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
Api->RemoveHooks();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove RenderDoc’s crash handler from the target process.<br/>
|
||||
/// If you have your own crash handler that you want to handle any exceptions,
|
||||
/// RenderDoc’s handler could interfere; so it can be disabled.
|
||||
/// </summary>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static void UnloadCrashHandler()
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
Api->UnloadCrashHandler();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger a capture as if the user had pressed one of the capture hotkeys.<br/>
|
||||
/// The capture will be taken from the next frame presented to whichever window is considered current.
|
||||
/// </summary>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static void TriggerCapture()
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
Api->TriggerCapture();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the details of all frame capture in the current session.
|
||||
/// This simply calls <see cref="GetCapture"/> for each index available as specified by <see cref="CaptureCount"/>.
|
||||
/// </summary>
|
||||
/// <returns>An immutable array of structs representing RenderDoc Captures.</returns>
|
||||
public static ImmutableArray<Capture> GetCaptures()
|
||||
{
|
||||
if (Api is null) return [];
|
||||
int captureCount = CaptureCount;
|
||||
if (captureCount is 0) return [];
|
||||
|
||||
ImmutableArray<Capture>.Builder captures = ImmutableArray.CreateBuilder<Capture>(captureCount);
|
||||
|
||||
for (int captureIndex = 0; captureIndex < captureCount; captureIndex++)
|
||||
{
|
||||
if (GetCapture(captureIndex) is { } capture)
|
||||
captures.Add(capture);
|
||||
}
|
||||
|
||||
return captures.DrainToImmutable();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the details of a particular frame capture, as specified by an index from 0 to <see cref="CaptureCount"/> - 1.
|
||||
/// </summary>
|
||||
/// <param name="index">specifies which capture to return the details of. Must be less than the value returned by <see cref="CaptureCount"/>.</param>
|
||||
/// <returns>A struct representing a RenderDoc Capture.</returns>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static Capture? GetCapture(int index)
|
||||
{
|
||||
if (Api is null) return null;
|
||||
|
||||
int length = 0;
|
||||
if (Api->GetCapture(index, null, &length, null) == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Span<byte> bytes = stackalloc byte[length + 1];
|
||||
long timestamp;
|
||||
|
||||
fixed (byte* ptr = bytes)
|
||||
Api->GetCapture(index, ptr, &length, ×tamp);
|
||||
|
||||
string fileName = Encoding.UTF8.GetString(bytes[length..]);
|
||||
return new Capture(index, fileName, DateTimeOffset.FromUnixTimeSeconds(timestamp).DateTime);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determine the closest matching replay UI executable for the current RenderDoc module, and launch it.
|
||||
/// </summary>
|
||||
/// <param name="connectTargetControl">if the UI should immediately connect to the application.</param>
|
||||
/// <param name="commandLine">string to be appended to the command line, e.g. a capture filename. If this parameter is null, the command line will be unmodified.</param>
|
||||
/// <returns>true if the UI was successfully launched; false otherwise.</returns>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static bool LaunchReplayUI(bool connectTargetControl, string? commandLine = null)
|
||||
{
|
||||
if (Api is null) return false;
|
||||
|
||||
if (commandLine == null)
|
||||
{
|
||||
return Api->LaunchReplayUI(connectTargetControl ? 1u : 0u, null) != 0;
|
||||
}
|
||||
|
||||
fixed (byte* ptr = commandLine.ToNullTerminatedByteArray())
|
||||
{
|
||||
return Api->LaunchReplayUI(connectTargetControl ? 1u : 0u, ptr) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Explicitly sets which window is considered active.<br/>
|
||||
/// The active window is the one that will be captured when the keybind to trigger a capture is pressed.
|
||||
/// </summary>
|
||||
/// <param name="hDevice">a handle to the API ‘device’ object that will be set active. Must be valid.</param>
|
||||
/// <param name="hWindow">a handle to the platform window handle that will be set active. Must be valid.</param>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static void SetActiveWindow(nint hDevice, nint hWindow)
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
Api->SetActiveWindow((void*)hDevice, (void*)hWindow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately begin a capture for the specified device/window combination.
|
||||
/// </summary>
|
||||
/// <param name="hDevice">a handle to the API ‘device’ object that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
|
||||
/// <param name="hWindow">a handle to the platform window handle that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static void StartFrameCapture(nint hDevice, nint hWindow)
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
Api->StartFrameCapture((void*)hDevice, (void*)hWindow);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately end an active capture for the specified device/window combination.
|
||||
/// </summary>
|
||||
/// <param name="hDevice">a handle to the API ‘device’ object that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
|
||||
/// <param name="hWindow">a handle to the platform window handle that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
|
||||
/// <returns>true if the capture succeeded; false otherwise.</returns>
|
||||
[RenderDocApiVersion(1, 0)]
|
||||
public static bool EndFrameCapture(nint hDevice, nint hWindow)
|
||||
{
|
||||
if (Api is null) return false;
|
||||
|
||||
return Api->EndFrameCapture((void*)hDevice, (void*)hWindow) != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Trigger multiple sequential frame captures as if the user had pressed one of the capture hotkeys before each frame.<br/>
|
||||
/// The captures will be taken from the next frames presented to whichever window is considered current.<br/>
|
||||
/// Each capture will be taken independently and saved to a separate file, with no reference to the other frames.
|
||||
/// </summary>
|
||||
/// <param name="numFrames">the number of frames to capture.</param>
|
||||
/// <remarks>Requires RenderDoc API version 1.1</remarks>
|
||||
[RenderDocApiVersion(1, 1)]
|
||||
public static void TriggerMultiFrameCapture(uint numFrames)
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
AssertAtLeast(1, 1);
|
||||
Api->TriggerMultiFrameCapture(numFrames);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an arbitrary comments field to the most recent capture,
|
||||
/// which will then be displayed in the UI to anyone opening the capture.
|
||||
/// <br/><br/>
|
||||
/// This is equivalent to calling <see cref="SetCaptureFileComments"/> with a null first (fileName) parameter.
|
||||
/// </summary>
|
||||
/// <param name="comments">the comments to set in the capture file.</param>
|
||||
/// <remarks>Requires RenderDoc API version 1.2</remarks>
|
||||
public static void SetMostRecentCaptureFileComments(string comments)
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
AssertAtLeast(1, 2);
|
||||
|
||||
byte[] commentBytes = comments.ToNullTerminatedByteArray();
|
||||
|
||||
fixed (byte* pcomment = commentBytes)
|
||||
{
|
||||
Api->SetCaptureFileComments((byte*)nint.Zero, pcomment);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an arbitrary comments field to an existing capture on disk,
|
||||
/// which will then be displayed in the UI to anyone opening the capture.
|
||||
/// </summary>
|
||||
/// <param name="fileName">the path to the capture file to set comments in. If this path is null or an empty string, the most recent capture file that has been created will be used.</param>
|
||||
/// <param name="comments">the comments to set in the capture file.</param>
|
||||
/// <remarks>Requires RenderDoc API version 1.2</remarks>
|
||||
[RenderDocApiVersion(1, 2)]
|
||||
public static void SetCaptureFileComments(string? fileName, string comments)
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
AssertAtLeast(1, 2);
|
||||
|
||||
byte[] commentBytes = comments.ToNullTerminatedByteArray();
|
||||
|
||||
fixed (byte* pcomment = commentBytes)
|
||||
{
|
||||
if (fileName is null)
|
||||
{
|
||||
Api->SetCaptureFileComments((byte*)nint.Zero, pcomment);
|
||||
}
|
||||
else
|
||||
{
|
||||
byte[] fileBytes = fileName.ToNullTerminatedByteArray();
|
||||
|
||||
fixed (byte* pfile = fileBytes)
|
||||
{
|
||||
Api->SetCaptureFileComments(pfile, pcomment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Similar to <see cref="EndFrameCapture"/>, but the capture contents will be discarded immediately, and not processed and written to disk.<br/>
|
||||
/// This will be more efficient than <see cref="EndFrameCapture"/> if the frame capture is not needed.
|
||||
/// </summary>
|
||||
/// <param name="hDevice">a handle to the API ‘device’ object that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
|
||||
/// <param name="hWindow">a handle to the platform window handle that will be set active. May be <see cref="nint.Zero"/> to wildcard match.</param>
|
||||
/// <returns>true if the capture was discarded; false if there was an error or no capture was in progress.</returns>
|
||||
/// <remarks>Requires RenderDoc API version 1.4</remarks>
|
||||
[RenderDocApiVersion(1, 4)]
|
||||
public static bool DiscardFrameCapture(nint hDevice, nint hWindow)
|
||||
{
|
||||
if (Api is null) return false;
|
||||
|
||||
AssertAtLeast(1, 4);
|
||||
return Api->DiscardFrameCapture((void*)hDevice, (void*)hWindow) != 0;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Requests that the currently connected replay UI raise its window to the top.<br/>
|
||||
/// This is only possible if an instance of the replay UI is currently connected, otherwise this method does nothing.<br/>
|
||||
/// This can be used in conjunction with <see cref="IsTargetControlConnected"/> and <see cref="LaunchReplayUI"/>,<br/> to intelligently handle showing the UI after making a capture.<br/><br/>
|
||||
/// Given OS differences, it is not guaranteed that the UI will be successfully raised even if the request is passed on.<br/>
|
||||
/// On some systems it may only be highlighted or otherwise indicated to the user.
|
||||
/// </summary>
|
||||
/// <returns>true if the request was passed onto the UI successfully; false if there is no UI connected or some other error occurred.</returns>
|
||||
/// <remarks>Requires RenderDoc API version 1.5</remarks>
|
||||
[RenderDocApiVersion(1, 5)]
|
||||
public static bool ShowReplayUI()
|
||||
{
|
||||
if (Api is null) return false;
|
||||
|
||||
AssertAtLeast(1, 5);
|
||||
return Api->ShowReplayUI() != 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a given title for the currently in-progress capture, which will be displayed in the UI.<br/>
|
||||
/// This can be used either with a user-defined capture using a manual start and end,
|
||||
/// or an automatic capture triggered by <see cref="TriggerCapture"/> or a keypress.<br/>
|
||||
/// If multiple captures are ongoing at once, the title will be applied to the first capture to end only.<br/>
|
||||
/// Any subsequent captures will not get any title unless the function is called again.
|
||||
/// This function can only be called while a capture is in-progress,
|
||||
/// after <see cref="StartFrameCapture"/> and before <see cref="EndFrameCapture"/>.<br/>
|
||||
/// If it is called elsewhere it will have no effect.
|
||||
/// If it is called multiple times within a capture, only the last title will have any effect.
|
||||
/// </summary>
|
||||
/// <param name="title">The title to set for the in-progress capture.</param>
|
||||
/// <remarks>Requires RenderDoc API version 1.6</remarks>
|
||||
[RenderDocApiVersion(1, 6)]
|
||||
public static void SetCaptureTitle(string title)
|
||||
{
|
||||
if (Api is null) return;
|
||||
|
||||
AssertAtLeast(1, 6);
|
||||
fixed (byte* ptr = title.ToNullTerminatedByteArray())
|
||||
Api->SetCaptureTitle(ptr);
|
||||
}
|
||||
|
||||
#region Dynamic Library loading
|
||||
|
||||
/// <summary>
|
||||
/// Reload the internal RenderDoc API structure. Useful for manually refreshing <see cref="Api"/> while using process injection.
|
||||
/// </summary>
|
||||
/// <param name="ignoreAlreadyLoaded">Ignores the existing API function structure and overwrites it with a re-request.</param>
|
||||
/// <param name="requiredVersion">The version of the RenderDoc API required by your application.</param>
|
||||
public static void ReloadApi(bool ignoreAlreadyLoaded = false, RenderDocVersion? requiredVersion = null)
|
||||
{
|
||||
if (_loaded && !ignoreAlreadyLoaded)
|
||||
return;
|
||||
|
||||
lock (typeof(RenderDoc))
|
||||
{
|
||||
// Prevent double loads.
|
||||
if (_loaded && !ignoreAlreadyLoaded)
|
||||
return;
|
||||
|
||||
if (requiredVersion.HasValue)
|
||||
MinimumRequired = requiredVersion.Value;
|
||||
|
||||
_loaded = true;
|
||||
_api = GetApi(MinimumRequired);
|
||||
|
||||
if (_api != null)
|
||||
AssertAtLeast(MinimumRequired);
|
||||
}
|
||||
}
|
||||
|
||||
private static RenderDocApi* _api = null;
|
||||
private static bool _loaded;
|
||||
|
||||
private static RenderDocApi* Api
|
||||
{
|
||||
get
|
||||
{
|
||||
ReloadApi();
|
||||
return _api;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Regex _dynamicLibraryPattern = RenderDocApiDynamicLibraryRegex();
|
||||
|
||||
private static RenderDocApi* GetApi(RenderDocVersion minimumRequired = RenderDocVersion.Version_1_0_0)
|
||||
{
|
||||
foreach (ProcessModule module in Process.GetCurrentProcess().Modules)
|
||||
{
|
||||
string moduleName = module.FileName ?? string.Empty;
|
||||
|
||||
if (!_dynamicLibraryPattern.IsMatch(moduleName))
|
||||
continue;
|
||||
|
||||
if (!NativeLibrary.TryLoad(moduleName, out nint moduleHandle))
|
||||
return null;
|
||||
|
||||
if (!NativeLibrary.TryGetExport(moduleHandle, "RENDERDOC_GetAPI", out nint procAddress))
|
||||
return null;
|
||||
|
||||
var RENDERDOC_GetApi = (delegate* unmanaged[Cdecl]<RenderDocVersion, RenderDocApi**, int>)procAddress;
|
||||
|
||||
RenderDocApi* api;
|
||||
return RENDERDOC_GetApi(minimumRequired, &api) != 0 ? api : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void AssertAtLeast(RenderDocVersion rdv, [CallerMemberName] string callee = "")
|
||||
{
|
||||
Version ver = rdv.SystemVersion;
|
||||
AssertAtLeast(ver.Major, ver.Minor, ver.Build, callee);
|
||||
}
|
||||
|
||||
private static void AssertAtLeast(int major, int minor, int patch = 0, [CallerMemberName] string callee = "")
|
||||
{
|
||||
if (!AssertVersionEnabled)
|
||||
return;
|
||||
|
||||
if (Version!.Major < major)
|
||||
goto fail;
|
||||
|
||||
if (Version.Major > major)
|
||||
goto success;
|
||||
if (Version.Minor < minor)
|
||||
goto fail;
|
||||
if (Version.Minor > minor)
|
||||
goto success;
|
||||
if (Version.Build < patch)
|
||||
goto fail;
|
||||
|
||||
success:
|
||||
return;
|
||||
|
||||
fail:
|
||||
Version minVersion =
|
||||
typeof(RenderDoc).GetMethod(callee)!.GetCustomAttribute<RenderDocApiVersionAttribute>()!.MinVersion;
|
||||
throw new NotSupportedException(
|
||||
$"This API was introduced in RenderDoc API {minVersion}. Current API version is {Version}.");
|
||||
}
|
||||
|
||||
private static byte[] ToNullTerminatedByteArray(this string str, Encoding? encoding = null)
|
||||
{
|
||||
encoding ??= Encoding.UTF8;
|
||||
|
||||
return encoding.GetBytes(str + '\0');
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"(lib)?renderdoc(\.dll|\.so|\.dylib)(\.\d+)?",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex RenderDocApiDynamicLibraryRegex();
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
51
src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs
Normal file
51
src/Ryujinx.Graphics.RenderDocApi/RenderDocApi.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
namespace Ryujinx.Graphics.RenderDocApi
|
||||
{
|
||||
#pragma warning disable CS0649
|
||||
internal unsafe struct RenderDocApi
|
||||
{
|
||||
public delegate* unmanaged[Cdecl]<int*, int*, int*, void> GetApiVersion;
|
||||
|
||||
public delegate* unmanaged[Cdecl]<CaptureOption, uint, int> SetCaptureOptionU32;
|
||||
public delegate* unmanaged[Cdecl]<CaptureOption, float, int> SetCaptureOptionF32;
|
||||
public delegate* unmanaged[Cdecl]<CaptureOption, uint> GetCaptureOptionU32;
|
||||
public delegate* unmanaged[Cdecl]<CaptureOption, float> GetCaptureOptionF32;
|
||||
|
||||
public delegate* unmanaged[Cdecl]<InputButton*, int, void> SetFocusToggleKeys;
|
||||
public delegate* unmanaged[Cdecl]<InputButton*, int, void> SetCaptureKeys;
|
||||
|
||||
public delegate* unmanaged[Cdecl]<OverlayBits> GetOverlayBits;
|
||||
public delegate* unmanaged[Cdecl]<OverlayBits, OverlayBits, void> MaskOverlayBits;
|
||||
|
||||
public delegate* unmanaged[Cdecl]<void> RemoveHooks;
|
||||
public delegate* unmanaged[Cdecl]<void> UnloadCrashHandler;
|
||||
public delegate* unmanaged[Cdecl]<byte*, void> SetCaptureFilePathTemplate;
|
||||
public delegate* unmanaged[Cdecl]<byte*> GetCaptureFilePathTemplate;
|
||||
|
||||
public delegate* unmanaged[Cdecl]<int> GetNumCaptures;
|
||||
public delegate* unmanaged[Cdecl]<int, byte*, int*, long*, uint> GetCapture;
|
||||
public delegate* unmanaged[Cdecl]<void> TriggerCapture;
|
||||
public delegate* unmanaged[Cdecl]<uint> IsTargetControlConnected;
|
||||
public delegate* unmanaged[Cdecl]<uint, byte*, uint> LaunchReplayUI;
|
||||
|
||||
public delegate* unmanaged[Cdecl]<void*, void*, void> SetActiveWindow;
|
||||
public delegate* unmanaged[Cdecl]<void*, void*, void> StartFrameCapture;
|
||||
public delegate* unmanaged[Cdecl]<uint> IsFrameCapturing;
|
||||
public delegate* unmanaged[Cdecl]<void*, void*, uint> EndFrameCapture;
|
||||
|
||||
// 1.1
|
||||
public delegate* unmanaged[Cdecl]<uint, void> TriggerMultiFrameCapture;
|
||||
|
||||
// 1.2
|
||||
public delegate* unmanaged[Cdecl]<byte*, byte*, void> SetCaptureFileComments;
|
||||
|
||||
// 1.3
|
||||
public delegate* unmanaged[Cdecl]<void*, void*, uint> DiscardFrameCapture;
|
||||
|
||||
// 1.5
|
||||
public delegate* unmanaged[Cdecl]<uint> ShowReplayUI;
|
||||
|
||||
// 1.6
|
||||
public delegate* unmanaged[Cdecl]<byte*, void> SetCaptureTitle;
|
||||
}
|
||||
#pragma warning restore CS0649
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.RenderDocApi
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)]
|
||||
public sealed class RenderDocApiVersionAttribute : Attribute
|
||||
{
|
||||
public Version MinVersion { get; }
|
||||
|
||||
public RenderDocApiVersionAttribute(int major, int minor, int patch = 0)
|
||||
{
|
||||
MinVersion = new Version(major, minor, patch);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
src/Ryujinx.Graphics.RenderDocApi/RenderDocVersion.cs
Normal file
47
src/Ryujinx.Graphics.RenderDocApi/RenderDocVersion.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
|
||||
namespace Ryujinx.Graphics.RenderDocApi
|
||||
{
|
||||
public enum RenderDocVersion
|
||||
{
|
||||
Version_1_0_0 = 10000,
|
||||
Version_1_0_1 = 10001,
|
||||
Version_1_0_2 = 10002,
|
||||
Version_1_1_0 = 10100,
|
||||
Version_1_1_1 = 10101,
|
||||
Version_1_1_2 = 10102,
|
||||
Version_1_2_0 = 10200,
|
||||
Version_1_3_0 = 10300,
|
||||
Version_1_4_0 = 10400,
|
||||
Version_1_4_1 = 10401,
|
||||
Version_1_4_2 = 10402,
|
||||
Version_1_5_0 = 10500,
|
||||
Version_1_6_0 = 10600,
|
||||
}
|
||||
|
||||
public static partial class Helpers
|
||||
{
|
||||
extension(RenderDocVersion rdv)
|
||||
{
|
||||
public Version SystemVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
int i = (int)rdv;
|
||||
return new (i / 10000, (i % 10000) / 100, i % 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension(Version sv)
|
||||
{
|
||||
public RenderDocVersion RenderDocVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
return (RenderDocVersion)(sv.Major * 10000 + sv.Minor * 100 + sv.Build);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
32
src/Ryujinx.Graphics.Vulkan/Helpers.cs
Normal file
32
src/Ryujinx.Graphics.Vulkan/Helpers.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using Silk.NET.Vulkan;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ryujinx.Graphics.Vulkan
|
||||
{
|
||||
public static class Helpers
|
||||
{
|
||||
extension(Vk api)
|
||||
{
|
||||
/// <summary>
|
||||
/// C# implementation of the RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE macro from the RenderDoc API header, since we cannot use macros from C#.
|
||||
/// </summary>
|
||||
/// <returns>The dispatch table pointer, which sits as the first pointer-sized object in the memory pointed to by the <see cref="Vk"/>'s <see cref="Instance"/> pointer.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe void* GetRenderDocDevicePointer() =>
|
||||
api.CurrentInstance is not null
|
||||
? api.CurrentInstance.Value.GetRenderDocDevicePointer()
|
||||
: null;
|
||||
}
|
||||
|
||||
extension(Instance instance)
|
||||
{
|
||||
/// <summary>
|
||||
/// C# implementation of the RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE macro from the RenderDoc API header, since we cannot use macros from C#.
|
||||
/// </summary>
|
||||
/// <returns>The dispatch table pointer, which sits as the first pointer-sized object in the memory pointed to by the <see cref="Instance"/>'s pointer.</returns>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public unsafe void* GetRenderDocDevicePointer()
|
||||
=> (*((void**)(instance.Handle)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ namespace Ryujinx.Graphics.Vulkan.MoltenVK
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
IntPtr configSize = (nint)Marshal.SizeOf<MVKConfiguration>();
|
||||
nint configSize = (nint)Marshal.SizeOf<MVKConfiguration>();
|
||||
|
||||
vkGetMoltenVKConfigurationMVK(nint.Zero, out MVKConfiguration config, configSize);
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
enabledExtensions = enabledExtensions.Append(ExtDebugUtils.ExtensionName).ToArray();
|
||||
}
|
||||
|
||||
IntPtr appName = Marshal.StringToHGlobalAnsi(AppName);
|
||||
nint appName = Marshal.StringToHGlobalAnsi(AppName);
|
||||
|
||||
ApplicationInfo applicationInfo = new()
|
||||
{
|
||||
@@ -166,7 +166,7 @@ namespace Ryujinx.Graphics.Vulkan
|
||||
|
||||
internal static DeviceInfo[] GetSuitablePhysicalDevices(Vk api)
|
||||
{
|
||||
IntPtr appName = Marshal.StringToHGlobalAnsi(AppName);
|
||||
nint appName = Marshal.StringToHGlobalAnsi(AppName);
|
||||
|
||||
ApplicationInfo applicationInfo = new()
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.Memory;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Cpu;
|
||||
using Ryujinx.HLE.Exceptions;
|
||||
using Ryujinx.HLE.HOS.Ipc;
|
||||
using Ryujinx.HLE.HOS.Kernel.Threading;
|
||||
using Ryujinx.HLE.HOS.Services.Ldn.Types;
|
||||
@@ -14,6 +15,7 @@ using Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.Types;
|
||||
using Ryujinx.Horizon.Common;
|
||||
using Ryujinx.Memory;
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
@@ -487,6 +489,23 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandCmif(106)] // 20.0.0+
|
||||
// SetProtocol
|
||||
public ResultCode SetProtocol(ServiceCtx context)
|
||||
{
|
||||
uint protocolValue = context.RequestData.ReadUInt32();
|
||||
|
||||
// On NX only input value 1 or 3 is allowed, with an error being thrown otherwise.
|
||||
|
||||
if (protocolValue != 1 && protocolValue != 3)
|
||||
{
|
||||
throw new ArgumentException($"{GetType().FullName}: Protocol value is not 1 or 3!! Protocol value: {protocolValue}");
|
||||
}
|
||||
|
||||
Logger.Stub?.PrintStub(LogClass.ServiceLdn, $"Protocol value: {protocolValue}");
|
||||
return ResultCode.Success;
|
||||
}
|
||||
|
||||
[CommandCmif(200)]
|
||||
// OpenAccessPoint()
|
||||
public ResultCode OpenAccessPoint(ServiceCtx context)
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace Ryujinx.HLE.HOS.Services.Sockets.Bsd.Impl
|
||||
|
||||
public bool Blocking { get => Socket.Blocking; set => Socket.Blocking = value; }
|
||||
|
||||
public nint Handle => IntPtr.Zero;
|
||||
public nint Handle => nint.Zero;
|
||||
|
||||
public IPEndPoint RemoteEndPoint => Socket.RemoteEndPoint as IPEndPoint;
|
||||
|
||||
|
||||
@@ -33,7 +33,20 @@ namespace Ryujinx.Horizon.Prepo.Ipc
|
||||
[CmifCommand(10100)] // 1.0.0-5.1.0
|
||||
[CmifCommand(10102)] // 6.0.0-9.2.0
|
||||
[CmifCommand(10104)] // 10.0.0+
|
||||
public Result SaveReport([Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid)
|
||||
public Result SaveReportOld([Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid)
|
||||
{
|
||||
if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0)
|
||||
{
|
||||
return PrepoResult.PermissionDenied;
|
||||
}
|
||||
|
||||
ProcessPlayReport(PlayReportKind.Normal, gameRoomBuffer, reportBuffer, pid, Uid.Null);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
[CmifCommand(10106)] // 21.0.0+
|
||||
public Result SaveReport([Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid, bool optInCheckEnabled)
|
||||
{
|
||||
if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0)
|
||||
{
|
||||
@@ -48,7 +61,20 @@ namespace Ryujinx.Horizon.Prepo.Ipc
|
||||
[CmifCommand(10101)] // 1.0.0-5.1.0
|
||||
[CmifCommand(10103)] // 6.0.0-9.2.0
|
||||
[CmifCommand(10105)] // 10.0.0+
|
||||
public Result SaveReportWithUser(Uid userId, [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid)
|
||||
public Result SaveReportWithUserOld(Uid userId, [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid)
|
||||
{
|
||||
if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0)
|
||||
{
|
||||
return PrepoResult.PermissionDenied;
|
||||
}
|
||||
|
||||
ProcessPlayReport(PlayReportKind.Normal, gameRoomBuffer, reportBuffer, pid, userId, true);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
|
||||
[CmifCommand(10107)] // 21.0.0+
|
||||
public Result SaveReportWithUser(Uid userId, [Buffer(HipcBufferFlags.In | HipcBufferFlags.Pointer)] ReadOnlySpan<byte> gameRoomBuffer, [Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias)] ReadOnlySpan<byte> reportBuffer, [ClientProcessId] ulong pid, bool optInCheckEnabled)
|
||||
{
|
||||
if ((_permissionLevel & PrepoServicePermissionLevel.User) == 0)
|
||||
{
|
||||
|
||||
@@ -8,8 +8,10 @@ namespace Ryujinx.Horizon.Sdk.Prepo
|
||||
{
|
||||
interface IPrepoService : IServiceObject
|
||||
{
|
||||
Result SaveReport(ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid);
|
||||
Result SaveReportWithUser(Uid userId, ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid);
|
||||
Result SaveReportOld(ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid);
|
||||
Result SaveReport(ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid, bool optInCheckEnabled);
|
||||
Result SaveReportWithUserOld(Uid userId, ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid);
|
||||
Result SaveReportWithUser(Uid userId, ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid, bool optInCheckEnabled);
|
||||
Result RequestImmediateTransmission();
|
||||
Result GetTransmissionStatus(out int status);
|
||||
Result GetSystemSessionId(out ulong systemSessionId);
|
||||
|
||||
@@ -9,10 +9,20 @@ using static SDL.SDL3;
|
||||
|
||||
namespace Ryujinx.Input.SDL3
|
||||
{
|
||||
|
||||
|
||||
public unsafe class SDL3GamepadDriver : IGamepadDriver
|
||||
{
|
||||
private readonly Dictionary<SDL_JoystickID, string> _gamepadsInstanceIdsMapping;
|
||||
private readonly Dictionary<SDL_JoystickID, string> _gamepadsIds;
|
||||
/// <summary>
|
||||
/// Unlinked joy-cons
|
||||
/// </summary>
|
||||
private readonly Dictionary<SDL_JoystickID, string> _joyConsIds;
|
||||
/// <summary>
|
||||
/// Linked joy-cons, remove dual joy-con from <c>_gamepadsIds</c> when a linked joy-con is removed
|
||||
/// </summary>
|
||||
private readonly Dictionary<SDL_JoystickID,string> _linkedJoyConsIds;
|
||||
private readonly Lock _lock = new();
|
||||
|
||||
public ReadOnlySpan<string> GamepadsIds
|
||||
@@ -21,7 +31,11 @@ namespace Ryujinx.Input.SDL3
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _gamepadsIds.Values.ToArray();
|
||||
List<string> temp = [];
|
||||
temp.AddRange(_gamepadsIds.Values);
|
||||
temp.AddRange(_joyConsIds.Values);
|
||||
temp.AddRange(_linkedJoyConsIds.Values);
|
||||
return temp.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +49,8 @@ namespace Ryujinx.Input.SDL3
|
||||
{
|
||||
_gamepadsInstanceIdsMapping = new Dictionary<SDL_JoystickID, string>();
|
||||
_gamepadsIds = [];
|
||||
_joyConsIds = [];
|
||||
_linkedJoyConsIds = [];
|
||||
|
||||
SDL3Driver.Instance.Initialize();
|
||||
SDL3Driver.Instance.OnJoyStickConnected += HandleJoyStickConnected;
|
||||
@@ -92,7 +108,7 @@ namespace Ryujinx.Input.SDL3
|
||||
int guidIndex = 0;
|
||||
id = guidIndex + "-" + guidString;
|
||||
|
||||
while (_gamepadsIds.ContainsValue(id))
|
||||
while (_gamepadsIds.ContainsValue(id) || _joyConsIds.ContainsValue(id) || _linkedJoyConsIds.ContainsValue(id))
|
||||
{
|
||||
id = (++guidIndex) + "-" + guidString;
|
||||
}
|
||||
@@ -104,16 +120,47 @@ namespace Ryujinx.Input.SDL3
|
||||
private void HandleJoyStickDisconnected(SDL_JoystickID joystickInstanceId)
|
||||
{
|
||||
bool joyConPairDisconnected = false;
|
||||
string fakeId = null;
|
||||
|
||||
if (!_gamepadsInstanceIdsMapping.Remove(joystickInstanceId, out string id))
|
||||
return;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_gamepadsIds.Remove(joystickInstanceId);
|
||||
if (!SDL3JoyConPair.IsCombinable(_gamepadsIds))
|
||||
if (!_linkedJoyConsIds.ContainsKey(joystickInstanceId))
|
||||
{
|
||||
_gamepadsIds.Remove(GetInstanceIdFromId(SDL3JoyConPair.Id));
|
||||
if (!_joyConsIds.Remove(joystickInstanceId))
|
||||
{
|
||||
_gamepadsIds.Remove(joystickInstanceId);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (string matchId in _gamepadsIds.Values)
|
||||
{
|
||||
if (matchId.Contains(id))
|
||||
{
|
||||
fakeId = matchId;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
string leftId = fakeId!.Split('_')[0];
|
||||
string rightId = fakeId!.Split('_')[1];
|
||||
|
||||
if (leftId == id)
|
||||
{
|
||||
_linkedJoyConsIds.Remove(GetInstanceIdFromId(rightId));
|
||||
_joyConsIds.Add(GetInstanceIdFromId(rightId), rightId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_linkedJoyConsIds.Remove(GetInstanceIdFromId(leftId));
|
||||
_joyConsIds.Add(GetInstanceIdFromId(leftId), leftId);
|
||||
}
|
||||
|
||||
_linkedJoyConsIds.Remove(joystickInstanceId);
|
||||
_gamepadsIds.Remove(GetInstanceIdFromId(fakeId));
|
||||
joyConPairDisconnected = true;
|
||||
}
|
||||
}
|
||||
@@ -121,13 +168,14 @@ namespace Ryujinx.Input.SDL3
|
||||
OnGamepadDisconnected?.Invoke(id);
|
||||
if (joyConPairDisconnected)
|
||||
{
|
||||
OnGamepadDisconnected?.Invoke(SDL3JoyConPair.Id);
|
||||
OnGamepadDisconnected?.Invoke(fakeId);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleJoyStickConnected(SDL_JoystickID joystickInstanceId)
|
||||
{
|
||||
bool joyConPairConnected = false;
|
||||
string fakeId = null;
|
||||
|
||||
if (SDL_IsGamepad(joystickInstanceId))
|
||||
{
|
||||
@@ -149,27 +197,40 @@ namespace Ryujinx.Input.SDL3
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
|
||||
_gamepadsIds.Add(joystickInstanceId, id);
|
||||
|
||||
if (SDL3JoyConPair.IsCombinable(_gamepadsIds))
|
||||
if (!SDL3JoyCon.IsJoyCon(joystickInstanceId))
|
||||
{
|
||||
// TODO - It appears that you can only have one joy con pair connected at a time?
|
||||
// This was also the behavior before SDL3
|
||||
_gamepadsIds.Remove(GetInstanceIdFromId(SDL3JoyConPair.Id));
|
||||
uint fakeInstanceID = uint.MaxValue;
|
||||
while (!_gamepadsIds.TryAdd((SDL_JoystickID)fakeInstanceID, SDL3JoyConPair.Id))
|
||||
_gamepadsIds.Add(joystickInstanceId, id);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (SDL3JoyConPair.IsCombinable(joystickInstanceId, _joyConsIds, out SDL_JoystickID match))
|
||||
{
|
||||
fakeInstanceID--;
|
||||
_joyConsIds.Remove(match, out string matchId);
|
||||
_linkedJoyConsIds.Add(joystickInstanceId, id);
|
||||
_linkedJoyConsIds.Add(match, matchId);
|
||||
|
||||
uint fakeInstanceId = uint.MaxValue;
|
||||
fakeId = SDL3JoyCon.IsLeftJoyCon(joystickInstanceId)
|
||||
? $"{id}_{matchId}"
|
||||
: $"{matchId}_{id}";
|
||||
while (!_gamepadsIds.TryAdd((SDL_JoystickID)fakeInstanceId, fakeId))
|
||||
{
|
||||
fakeInstanceId--;
|
||||
}
|
||||
_gamepadsInstanceIdsMapping.Add((SDL_JoystickID)fakeInstanceId, fakeId);
|
||||
joyConPairConnected = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_joyConsIds.Add(joystickInstanceId, id);
|
||||
}
|
||||
joyConPairConnected = true;
|
||||
}
|
||||
}
|
||||
|
||||
OnGamepadConnected?.Invoke(id);
|
||||
if (joyConPairConnected)
|
||||
{
|
||||
OnGamepadConnected?.Invoke(SDL3JoyConPair.Id);
|
||||
OnGamepadConnected?.Invoke(fakeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,10 +254,22 @@ namespace Ryujinx.Input.SDL3
|
||||
{
|
||||
OnGamepadDisconnected?.Invoke(gamepad.Value);
|
||||
}
|
||||
|
||||
foreach (var gamepad in _joyConsIds)
|
||||
{
|
||||
OnGamepadDisconnected?.Invoke(gamepad.Value);
|
||||
}
|
||||
|
||||
foreach (var gamepad in _linkedJoyConsIds)
|
||||
{
|
||||
OnGamepadDisconnected?.Invoke(gamepad.Value);
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_gamepadsIds.Clear();
|
||||
_joyConsIds.Clear();
|
||||
_linkedJoyConsIds.Clear();
|
||||
}
|
||||
|
||||
SDL3Driver.Instance.Dispose();
|
||||
@@ -215,11 +288,27 @@ namespace Ryujinx.Input.SDL3
|
||||
|
||||
public IGamepad GetGamepad(string id)
|
||||
{
|
||||
if (id == SDL3JoyConPair.Id)
|
||||
// joy-con pair ids is the combined ids of its parts which are split using a '_'
|
||||
if (id.Contains('_'))
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return SDL3JoyConPair.GetGamepad(_gamepadsIds);
|
||||
string leftId = id.Split('_')[0];
|
||||
string rightId = id.Split('_')[1];
|
||||
|
||||
SDL_JoystickID leftInstanceId = GetInstanceIdFromId(leftId);
|
||||
SDL_JoystickID rightInstanceId = GetInstanceIdFromId(rightId);
|
||||
|
||||
SDL_Gamepad* leftGamepadHandle = SDL_OpenGamepad(leftInstanceId);
|
||||
SDL_Gamepad* rightGamepadHandle = SDL_OpenGamepad(rightInstanceId);
|
||||
|
||||
if (leftGamepadHandle == null || rightGamepadHandle == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SDL3JoyConPair(new SDL3JoyCon(leftGamepadHandle, leftId),
|
||||
new SDL3JoyCon(rightGamepadHandle, rightId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +321,7 @@ namespace Ryujinx.Input.SDL3
|
||||
return null;
|
||||
}
|
||||
|
||||
if (SDL_GetGamepadName(gamepadHandle).StartsWith(SDL3JoyCon.Prefix))
|
||||
if (SDL3JoyCon.IsJoyCon(instanceId))
|
||||
{
|
||||
return new SDL3JoyCon(gamepadHandle, id);
|
||||
}
|
||||
@@ -249,6 +338,22 @@ namespace Ryujinx.Input.SDL3
|
||||
yield return GetGamepad(gamepad.Value);
|
||||
}
|
||||
}
|
||||
|
||||
lock (_joyConsIds)
|
||||
{
|
||||
foreach (var gamepad in _joyConsIds)
|
||||
{
|
||||
yield return GetGamepad(gamepad.Value);
|
||||
}
|
||||
}
|
||||
|
||||
lock (_linkedJoyConsIds)
|
||||
{
|
||||
foreach (var gamepad in _linkedJoyConsIds)
|
||||
{
|
||||
yield return GetGamepad(gamepad.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,10 +24,10 @@ namespace Ryujinx.Input.SDL3
|
||||
private readonly Dictionary<GamepadButtonInputId, SDL_GamepadButton> _leftButtonsDriverMapping = new()
|
||||
{
|
||||
{GamepadButtonInputId.LeftStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK},
|
||||
{GamepadButtonInputId.DpadUp, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH},
|
||||
{GamepadButtonInputId.DpadDown, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
{GamepadButtonInputId.DpadLeft, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST},
|
||||
{GamepadButtonInputId.DpadRight, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST},
|
||||
{GamepadButtonInputId.DpadUp, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST},
|
||||
{GamepadButtonInputId.DpadDown, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST},
|
||||
{GamepadButtonInputId.DpadLeft, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
{GamepadButtonInputId.DpadRight, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH},
|
||||
{GamepadButtonInputId.Minus, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START},
|
||||
{GamepadButtonInputId.LeftShoulder, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE1},
|
||||
{GamepadButtonInputId.LeftTrigger, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_PADDLE2},
|
||||
@@ -37,10 +37,10 @@ namespace Ryujinx.Input.SDL3
|
||||
private readonly Dictionary<GamepadButtonInputId, SDL_GamepadButton> _rightButtonsDriverMapping = new()
|
||||
{
|
||||
{GamepadButtonInputId.RightStick, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_LEFT_STICK},
|
||||
{GamepadButtonInputId.A, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST},
|
||||
{GamepadButtonInputId.B, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH},
|
||||
{GamepadButtonInputId.X, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
{GamepadButtonInputId.Y, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST},
|
||||
{GamepadButtonInputId.A, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_SOUTH},
|
||||
{GamepadButtonInputId.B, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_WEST},
|
||||
{GamepadButtonInputId.X, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_EAST},
|
||||
{GamepadButtonInputId.Y, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_NORTH},
|
||||
{GamepadButtonInputId.Plus, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_START},
|
||||
{GamepadButtonInputId.RightShoulder, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE1},
|
||||
{GamepadButtonInputId.RightTrigger, SDL_GamepadButton.SDL_GAMEPAD_BUTTON_RIGHT_PADDLE2},
|
||||
@@ -398,5 +398,15 @@ namespace Ryujinx.Input.SDL3
|
||||
|
||||
return SDL_GetGamepadButton(_gamepadHandle, button);
|
||||
}
|
||||
|
||||
public static bool IsJoyCon(SDL_JoystickID gamepadsId)
|
||||
{
|
||||
return SDL_GetGamepadNameForID(gamepadsId) is LeftName or RightName;
|
||||
}
|
||||
|
||||
public static bool IsLeftJoyCon(SDL_JoystickID gamepadsId)
|
||||
{
|
||||
return SDL_GetGamepadNameForID(gamepadsId) is LeftName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Ryujinx.Input.SDL3
|
||||
public const string Id = "JoyConPair";
|
||||
string IGamepad.Id => Id;
|
||||
|
||||
public string Name => "* Nintendo Switch Joy-Con (L/R)";
|
||||
public string Name => "Nintendo Switch Dual Joy-Con (L/R)";
|
||||
public bool IsConnected => left is { IsConnected: true } && right is { IsConnected: true };
|
||||
|
||||
public void Dispose()
|
||||
@@ -96,44 +96,23 @@ namespace Ryujinx.Input.SDL3
|
||||
right.SetTriggerThreshold(triggerThreshold);
|
||||
}
|
||||
|
||||
public static bool IsCombinable(Dictionary<SDL_JoystickID, string> gamepadsIds)
|
||||
public static bool IsCombinable(SDL_JoystickID joyCon1, Dictionary<SDL_JoystickID, string> joyConIds, out SDL_JoystickID match)
|
||||
{
|
||||
(int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds);
|
||||
return leftIndex >= 0 && rightIndex >= 0;
|
||||
}
|
||||
bool isLeft = SDL3JoyCon.IsLeftJoyCon(joyCon1);
|
||||
string matchName = isLeft ? SDL3JoyCon.RightName : SDL3JoyCon.LeftName;
|
||||
match = 0;
|
||||
|
||||
private static (int leftIndex, int rightIndex) DetectJoyConPair(Dictionary<SDL_JoystickID, string> gamepadsIds)
|
||||
{
|
||||
Dictionary<string, SDL_JoystickID> gamepadNames = gamepadsIds
|
||||
.Where(gamepadId => gamepadId.Value != Id && SDL_GetGamepadNameForID(gamepadId.Key) is SDL3JoyCon.LeftName or SDL3JoyCon.RightName)
|
||||
.Select(gamepad => (SDL_GetGamepadNameForID(gamepad.Key), gamepad.Key))
|
||||
.ToDictionary();
|
||||
SDL_JoystickID idx;
|
||||
int leftIndex = gamepadNames.TryGetValue(SDL3JoyCon.LeftName, out idx) ? (int)idx : -1;
|
||||
int rightIndex = gamepadNames.TryGetValue(SDL3JoyCon.RightName, out idx) ? (int)idx : -1;
|
||||
|
||||
return (leftIndex, rightIndex);
|
||||
}
|
||||
|
||||
public unsafe static IGamepad GetGamepad(Dictionary<SDL_JoystickID, string> gamepadsIds)
|
||||
{
|
||||
(int leftIndex, int rightIndex) = DetectJoyConPair(gamepadsIds);
|
||||
|
||||
if (leftIndex <= 0 || rightIndex <= 0)
|
||||
foreach (var joyConId in joyConIds.Keys)
|
||||
{
|
||||
return null;
|
||||
if (SDL_GetGamepadNameForID(joyConId) == matchName)
|
||||
{
|
||||
match = joyConId;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
SDL_Gamepad* leftGamepadHandle = SDL_OpenGamepad((SDL_JoystickID)leftIndex);
|
||||
SDL_Gamepad* rightGamepadHandle = SDL_OpenGamepad((SDL_JoystickID)rightIndex);
|
||||
|
||||
if (leftGamepadHandle == null || rightGamepadHandle == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SDL3JoyConPair(new SDL3JoyCon(leftGamepadHandle, gamepadsIds[(SDL_JoystickID)leftIndex]),
|
||||
new SDL3JoyCon(rightGamepadHandle, gamepadsIds[(SDL_JoystickID)rightIndex]));
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ namespace Ryujinx.Memory.WindowsShared
|
||||
{
|
||||
SplitForMap((ulong)location, (ulong)size, srcOffset);
|
||||
|
||||
IntPtr ptr = WindowsApi.MapViewOfFile3(
|
||||
nint ptr = WindowsApi.MapViewOfFile3(
|
||||
sharedMemory,
|
||||
WindowsApi.CurrentProcessHandle,
|
||||
location,
|
||||
|
||||
@@ -227,7 +227,7 @@ namespace Ryujinx.Tests.Memory
|
||||
|
||||
// Create some info to be used for managing the native writing loop.
|
||||
int stateSize = Unsafe.SizeOf<NativeWriteLoopState>();
|
||||
IntPtr statePtr = Marshal.AllocHGlobal(stateSize);
|
||||
nint statePtr = Marshal.AllocHGlobal(stateSize);
|
||||
Unsafe.InitBlockUnaligned((void*)statePtr, 0, (uint)stateSize);
|
||||
|
||||
ref NativeWriteLoopState writeLoopState = ref Unsafe.AsRef<NativeWriteLoopState>((void*)statePtr);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
@@ -10,23 +12,34 @@ namespace Ryujinx.UI.LocaleGenerator
|
||||
{
|
||||
public void Initialize(IncrementalGeneratorInitializationContext context)
|
||||
{
|
||||
IncrementalValuesProvider<AdditionalText> localeFile = context.AdditionalTextsProvider.Where(static x => x.Path.EndsWith("locales.json"));
|
||||
IncrementalValuesProvider<AdditionalText> localeFiles = context.AdditionalTextsProvider.Where(static x => Path.GetDirectoryName(x.Path)?.Replace('\\', '/').EndsWith("assets/Locales") ?? false);
|
||||
|
||||
IncrementalValuesProvider<string> contents = localeFile.Select((text, cancellationToken) => text.GetText(cancellationToken)!.ToString());
|
||||
IncrementalValueProvider<ImmutableArray<(string, string)>> collectedContents = localeFiles.Select((text, cancellationToken) => (text.GetText(cancellationToken)!.ToString(), Path.GetFileName(text.Path))).Collect();
|
||||
|
||||
context.RegisterSourceOutput(contents, (spc, content) =>
|
||||
context.RegisterSourceOutput(collectedContents, (spc, contents) =>
|
||||
{
|
||||
IEnumerable<string> lines = content.Split('\n').Where(x => x.Trim().StartsWith("\"ID\":")).Select(x => x.Split(':')[1].Trim().Replace("\"", string.Empty).Replace(",", string.Empty));
|
||||
|
||||
StringBuilder enumSourceBuilder = new();
|
||||
enumSourceBuilder.AppendLine("namespace Ryujinx.Ava.Common.Locale;");
|
||||
enumSourceBuilder.AppendLine("public enum LocaleKeys");
|
||||
enumSourceBuilder.AppendLine("{");
|
||||
foreach (string? line in lines)
|
||||
|
||||
foreach ((string, string) content in contents)
|
||||
{
|
||||
enumSourceBuilder.AppendLine($" {line},");
|
||||
IEnumerable<string> lines = content.Item1.Split('\n').Where(x => x.Trim().StartsWith("\"ID\":")).Select(x => x.Split(':')[1].Trim().Replace("\"", string.Empty).Replace(",", string.Empty));
|
||||
|
||||
foreach (string? line in lines)
|
||||
{
|
||||
if (content.Item2 == "Root.json")
|
||||
{
|
||||
enumSourceBuilder.AppendLine($" {line},");
|
||||
}
|
||||
else
|
||||
{
|
||||
enumSourceBuilder.AppendLine($" {content.Item2.Split('.')[0]}_{line},");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enumSourceBuilder.AppendLine("}");
|
||||
|
||||
spc.AddSource("LocaleKeys", enumSourceBuilder.ToString());
|
||||
|
||||
@@ -8,6 +8,8 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Ava.Common.Locale
|
||||
@@ -158,52 +160,86 @@ namespace Ryujinx.Ava.Common.Locale
|
||||
LocaleChanged?.Invoke();
|
||||
}
|
||||
|
||||
private static LocalesJson? _localeData;
|
||||
private static LocalesData? _localeData;
|
||||
|
||||
private static Dictionary<LocaleKeys, string> LoadJsonLanguage(string languageCode)
|
||||
{
|
||||
Dictionary<LocaleKeys, string> localeStrings = new();
|
||||
|
||||
_localeData ??= EmbeddedResources.ReadAllText("Ryujinx/Assets/Locale.json")
|
||||
.Into(it => JsonHelper.Deserialize(it, LocalesJsonContext.Default.LocalesJson));
|
||||
|
||||
foreach (LocalesEntry locale in _localeData.Value.Locales)
|
||||
if (_localeData is null)
|
||||
{
|
||||
if (locale.Translations.Count < _localeData.Value.Languages.Count)
|
||||
Dictionary<string, LocalesJson> locales = [];
|
||||
|
||||
foreach (string uri in EmbeddedResources.GetAllAvailableResources("Ryujinx/Assets/Locales", ".json"))
|
||||
{
|
||||
throw new Exception(
|
||||
$"Locale key {{{locale.ID}}} is missing languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
|
||||
string path = uri[..^".json".Length];
|
||||
path = path.Replace('.', '/');
|
||||
path = path.Append(".json");
|
||||
|
||||
locales.TryAdd(Path.GetFileName(path), EmbeddedResources.ReadAllText(path)
|
||||
.Into(it => JsonHelper.Deserialize(it, LocalesJsonContext.Default.LocalesJson)));
|
||||
}
|
||||
|
||||
if (locale.Translations.Count > _localeData.Value.Languages.Count)
|
||||
|
||||
_localeData = new LocalesData
|
||||
{
|
||||
throw new Exception(
|
||||
$"Locale key {{{locale.ID}}} has too many languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
|
||||
}
|
||||
Languages = EmbeddedResources.ReadAllText("Ryujinx/Assets/Languages.json")
|
||||
.Into(it => JsonHelper.Deserialize(it, LanguagesJsonContext.Default.LanguagesJson)).Languages.Keys.ToList(),
|
||||
LocalesFiles = locales
|
||||
};
|
||||
|
||||
|
||||
if (!Enum.TryParse<LocaleKeys>(locale.ID, out LocaleKeys localeKey))
|
||||
continue;
|
||||
}
|
||||
|
||||
string str = locale.Translations.TryGetValue(languageCode, out string val) && !string.IsNullOrEmpty(val)
|
||||
? val
|
||||
: locale.Translations[DefaultLanguageCode];
|
||||
|
||||
if (string.IsNullOrEmpty(str))
|
||||
foreach ((string fileName, LocalesJson file) in _localeData.Value.LocalesFiles)
|
||||
{
|
||||
foreach (LocalesEntry locale in file.Locales)
|
||||
{
|
||||
throw new Exception(
|
||||
$"Locale key '{locale.ID}' has no valid translations for desired language {languageCode}! {DefaultLanguageCode} is an empty string or null");
|
||||
}
|
||||
if (locale.Translations.Count < _localeData.Value.Languages.Count)
|
||||
{
|
||||
throw new Exception(
|
||||
$"Locale key {{{locale.ID}}} is missing languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
|
||||
}
|
||||
|
||||
localeStrings[localeKey] = str;
|
||||
if (locale.Translations.Count > _localeData.Value.Languages.Count)
|
||||
{
|
||||
throw new Exception(
|
||||
$"Locale key {{{locale.ID}}} has too many languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!");
|
||||
}
|
||||
|
||||
if (!Enum.TryParse<LocaleKeys>(fileName == "Root.json" ? locale.ID : $"{fileName[..^".json".Length]}_{locale.ID}" , out LocaleKeys localeKey))
|
||||
continue;
|
||||
|
||||
string str = locale.Translations.TryGetValue(languageCode, out string val) && !string.IsNullOrEmpty(val)
|
||||
? val
|
||||
: locale.Translations[DefaultLanguageCode];
|
||||
|
||||
if (string.IsNullOrEmpty(str))
|
||||
{
|
||||
throw new Exception(
|
||||
$"Locale key '{locale.ID}' has no valid translations for desired language {languageCode}! {DefaultLanguageCode} is an empty string or null");
|
||||
}
|
||||
|
||||
localeStrings[localeKey] = str;
|
||||
}
|
||||
}
|
||||
|
||||
return localeStrings;
|
||||
}
|
||||
}
|
||||
|
||||
public struct LocalesJson
|
||||
public struct LocalesData
|
||||
{
|
||||
public List<string> Languages { get; set; }
|
||||
public Dictionary<string, LocalesJson> LocalesFiles { get; set; }
|
||||
}
|
||||
|
||||
public struct LanguagesJson
|
||||
{
|
||||
public Dictionary<string, string> Languages { get; set; }
|
||||
}
|
||||
|
||||
public struct LocalesJson
|
||||
{
|
||||
public List<LocalesEntry> Locales { get; set; }
|
||||
}
|
||||
|
||||
@@ -216,4 +252,8 @@ namespace Ryujinx.Ava.Common.Locale
|
||||
[JsonSourceGenerationOptions(WriteIndented = true)]
|
||||
[JsonSerializable(typeof(LocalesJson))]
|
||||
internal partial class LocalesJsonContext : JsonSerializerContext;
|
||||
|
||||
[JsonSourceGenerationOptions(WriteIndented = true)]
|
||||
[JsonSerializable(typeof(LanguagesJson))]
|
||||
internal partial class LanguagesJsonContext : JsonSerializerContext;
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ namespace Ryujinx.Ava.Input
|
||||
AvaKey.OemComma,
|
||||
AvaKey.OemPeriod,
|
||||
AvaKey.OemQuestion,
|
||||
AvaKey.OemBackslash,
|
||||
AvaKey.OemPipe,
|
||||
|
||||
// NOTE: invalid
|
||||
AvaKey.None
|
||||
|
||||
@@ -18,6 +18,7 @@ using Ryujinx.Common.GraphicsDriver;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.SystemInterop;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Graphics.RenderDocApi;
|
||||
using Ryujinx.Graphics.Vulkan.MoltenVK;
|
||||
using Ryujinx.Headless;
|
||||
using Ryujinx.SDL3.Common;
|
||||
|
||||
@@ -77,16 +77,18 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.RenderDocApi\Ryujinx.Graphics.RenderDocApi.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.Vulkan/Ryujinx.Graphics.Vulkan.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.OpenGL/Ryujinx.Graphics.OpenGL.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Input.SDL3\Ryujinx.Input.SDL3.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Audio.Backends.Apple\Ryujinx.Audio.Backends.Apple.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Audio.Backends.SDL3\Ryujinx.Audio.Backends.SDL3.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Audio.Backends.SoundIo\Ryujinx.Audio.Backends.SoundIo.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Common\Ryujinx.Common.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.HLE\Ryujinx.HLE.csproj" />
|
||||
<ProjectReference Include="..\ARMeilleure\ARMeilleure.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.OpenGL\Ryujinx.Graphics.OpenGL.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.Graphics.Gpu\Ryujinx.Graphics.Gpu.csproj" />
|
||||
<ProjectReference Include="..\Ryujinx.UI.LocaleGenerator\Ryujinx.UI.LocaleGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
@@ -134,7 +136,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="Assets\locales.json" />
|
||||
<None Remove="Assets\**\*.json" />
|
||||
<None Remove="Assets\Styles\Styles.xaml" />
|
||||
<None Remove="Assets\Styles\Themes.xaml" />
|
||||
<None Remove="Assets\Icons\Controller_JoyConLeft.svg" />
|
||||
@@ -156,8 +158,8 @@
|
||||
<EmbeddedResource Include="..\..\docs\compatibility.csv" LogicalName="RyujinxGameCompatibilityList">
|
||||
<Link>Assets\RyujinxGameCompatibility.csv</Link>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="..\..\assets\locales.json">
|
||||
<Link>Assets\Locale.json</Link>
|
||||
<EmbeddedResource Include="..\..\assets\**\*.json">
|
||||
<LinkBase>Assets</LinkBase>
|
||||
</EmbeddedResource>
|
||||
<EmbeddedResource Include="Assets\Styles\Styles.xaml" />
|
||||
<EmbeddedResource Include="Assets\Icons\Controller_JoyConLeft.svg" />
|
||||
@@ -178,6 +180,6 @@
|
||||
<EmbeddedResource Include="Assets\UIImages\Logo_Ryujinx_AntiAlias.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="..\..\assets\locales.json" />
|
||||
<AdditionalFiles Include="..\..\assets\Locales\*.json" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -6,6 +6,7 @@ using Avalonia.Threading;
|
||||
using DiscordRPC;
|
||||
using LibHac.Common;
|
||||
using LibHac.Ns;
|
||||
using Ryujinx.Audio.Backends.Apple;
|
||||
using Ryujinx.Audio.Backends.Dummy;
|
||||
using Ryujinx.Audio.Backends.OpenAL;
|
||||
using Ryujinx.Audio.Backends.SDL3;
|
||||
@@ -949,6 +950,9 @@ namespace Ryujinx.Ava.Systems
|
||||
AudioBackend.Dummy
|
||||
];
|
||||
|
||||
if (OperatingSystem.IsMacOS())
|
||||
availableBackends.Insert(0, AudioBackend.AudioToolbox);
|
||||
|
||||
AudioBackend preferredBackend = ConfigurationState.Instance.System.AudioBackend.Value;
|
||||
|
||||
if (preferredBackend is AudioBackend.SDL2)
|
||||
@@ -985,6 +989,9 @@ namespace Ryujinx.Ava.Systems
|
||||
|
||||
deviceDriver = currentBackend switch
|
||||
{
|
||||
#pragma warning disable CA1416 // Platform compatibility is enforced in AppleHardwareDeviceDriver.IsSupported, before any potentially platform-sensitive code can run.
|
||||
AudioBackend.AudioToolbox => InitializeAudioBackend<AppleHardwareDeviceDriver>(AudioBackend.AudioToolbox, nextBackend),
|
||||
#pragma warning restore CA1416
|
||||
AudioBackend.SDL3 => InitializeAudioBackend<SDL3HardwareDeviceDriver>(AudioBackend.SDL3, nextBackend),
|
||||
AudioBackend.SoundIo => InitializeAudioBackend<SoundIoHardwareDeviceDriver>(AudioBackend.SoundIo, nextBackend),
|
||||
AudioBackend.OpenAl => InitializeAudioBackend<OpenALHardwareDeviceDriver>(AudioBackend.OpenAl, nextBackend),
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace Ryujinx.Ava.Systems.Configuration
|
||||
OpenAl,
|
||||
SoundIo,
|
||||
SDL3,
|
||||
AudioToolbox,
|
||||
SDL2 = SDL3
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Systems.Update.Client;
|
||||
using Ryujinx.Systems.Update.Common;
|
||||
using System;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -46,6 +47,12 @@ namespace Ryujinx.Ava.Systems
|
||||
return Return<VersionResponse>.Failure(
|
||||
new MessageError("DNS resolution error occurred. Is your internet down?"));
|
||||
}
|
||||
catch (HttpRequestException hre)
|
||||
when (hre.StatusCode is HttpStatusCode.BadGateway)
|
||||
{
|
||||
return Return<VersionResponse>.Failure(
|
||||
new MessageError("Could not connect to the update server, but it appears like you have internet. It seems like the update server is offline, try again later."));
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<Optional<(Version Current, Version Incoming)>> CheckVersionAsync(bool showVersionUpToDate = false)
|
||||
|
||||
@@ -16,41 +16,31 @@
|
||||
<Design.DataContext>
|
||||
<viewModels:ProfileSelectorDialogViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RowDefinitions="*,Auto">
|
||||
|
||||
<Border
|
||||
CornerRadius="5"
|
||||
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
|
||||
BorderThickness="1">
|
||||
|
||||
<Border Padding="-3" BorderThickness="0">
|
||||
<ListBox
|
||||
MaxHeight="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Background="Transparent"
|
||||
ItemsSource="{Binding Profiles}"
|
||||
SelectionChanged="ProfilesList_SelectionChanged">
|
||||
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel
|
||||
HorizontalAlignment="Left"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal" />
|
||||
Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Margin" Value="5 5 0 5" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="5" />
|
||||
</Style>
|
||||
<Style Selector="Rectangle#SelectionIndicator">
|
||||
<Setter Property="Opacity" Value="0" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
|
||||
<ListBox.DataTemplates>
|
||||
<DataTemplate
|
||||
DataType="models:UserProfile">
|
||||
@@ -58,6 +48,7 @@
|
||||
PointerEntered="Grid_PointerEntered"
|
||||
PointerExited="Grid_OnPointerExited">
|
||||
<Border
|
||||
Margin="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ClipToBounds="True"
|
||||
@@ -69,37 +60,26 @@
|
||||
<Image
|
||||
Width="96"
|
||||
Height="96"
|
||||
Margin="0,0,0,10"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
Source="{Binding Image, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
MaxWidth="90"
|
||||
Text="{Binding Name}"
|
||||
Height="30"
|
||||
MaxWidth="90"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="2"
|
||||
Margin="5" />
|
||||
TextWrapping="Wrap"
|
||||
MaxLines="2" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
<DataTemplate
|
||||
DataType="viewModels:BaseModel">
|
||||
<Panel
|
||||
Height="118"
|
||||
Width="96">
|
||||
<Panel.Styles>
|
||||
<Style Selector="Panel">
|
||||
<Setter Property="Background" Value="{DynamicResource ListBoxBackground}" />
|
||||
</Style>
|
||||
</Panel.Styles>
|
||||
</Panel>
|
||||
</DataTemplate>
|
||||
</ListBox.DataTemplates>
|
||||
</ListBox>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -94,7 +94,7 @@ namespace Ryujinx.Ava.UI.Applet
|
||||
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle],
|
||||
Title = LocaleManager.Instance[LocaleKeys.UserProfiles_WindowTitle],
|
||||
PrimaryButtonText = LocaleManager.Instance[LocaleKeys.Continue],
|
||||
SecondaryButtonText = string.Empty,
|
||||
CloseButtonText = LocaleManager.Instance[LocaleKeys.Cancel],
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
Command="{Binding RunApplication}"
|
||||
CommandParameter="{Binding}"
|
||||
Header="{ext:Locale GameListContextMenuRunApplication}"
|
||||
Icon="{ext:Icon fa-solid fa-play}"/>
|
||||
Icon="{ext:Icon fa-solid fa-play}" />
|
||||
<MenuItem
|
||||
Command="{Binding ToggleFavorite}"
|
||||
CommandParameter="{Binding}"
|
||||
Header="{ext:Locale GameListContextMenuToggleFavorite}"
|
||||
Header="{Binding FavoriteStatusText}"
|
||||
Icon="{ext:Icon fa-solid fa-star}" />
|
||||
<MenuItem
|
||||
Command="{Binding CreateApplicationShortcut}"
|
||||
@@ -30,15 +30,15 @@
|
||||
ToolTip.Tip="{ext:Locale EditCustomConfigurationToolTip}" />
|
||||
<MenuItem
|
||||
Command="{Binding EditGameConfiguration}"
|
||||
CommandParameter="{Binding}"
|
||||
IsVisible="{Binding !SelectedApplication.HasIndependentConfiguration}"
|
||||
Header="{ext:Locale GameListContextMenuCreateCustomConfiguration}"
|
||||
Icon="{ext:Icon fa-solid fa-gear}"
|
||||
ToolTip.Tip="{ext:Locale CreateCustomConfigurationToolTip}" />
|
||||
CommandParameter="{Binding}"
|
||||
IsVisible="{Binding !SelectedApplication.HasIndependentConfiguration}"
|
||||
Header="{ext:Locale GameListContextMenuCreateCustomConfiguration}"
|
||||
Icon="{ext:Icon fa-solid fa-gear}"
|
||||
ToolTip.Tip="{ext:Locale CreateCustomConfigurationToolTip}" />
|
||||
<MenuItem
|
||||
IsVisible="{Binding HasCompatibilityEntry}"
|
||||
Command="{Binding OpenApplicationCompatibility}"
|
||||
CommandParameter="{Binding}"
|
||||
IsVisible="{Binding HasCompatibilityEntry}"
|
||||
Header="{ext:Locale GameListContextMenuShowCompatEntry}"
|
||||
Icon="{ext:Icon fa-solid fa-database}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuShowCompatEntryToolTip}"/>
|
||||
@@ -104,8 +104,8 @@
|
||||
Command="{Binding TrimXci}"
|
||||
CommandParameter="{Binding}"
|
||||
Header="{ext:Locale GameListContextMenuTrimXCI}"
|
||||
IsEnabled="{Binding TrimXCIEnabled}"
|
||||
Icon="{ext:Icon fa-solid fa-scissors}"
|
||||
IsEnabled="{Binding TrimXCIEnabled}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuTrimXCIToolTip}" />
|
||||
<MenuItem Header="{ext:Locale GameListContextMenuCacheManagement}" Icon="{ext:Icon fa-solid fa-memory}">
|
||||
<MenuItem
|
||||
@@ -151,9 +151,9 @@
|
||||
Header="{ext:Locale GameListContextMenuExtractDataRomFS}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataRomFSToolTip}" />
|
||||
<MenuItem
|
||||
IsVisible="{Binding HasDlc}"
|
||||
Command="{Binding ExtractApplicationAocRomFs}"
|
||||
CommandParameter="{Binding}"
|
||||
IsVisible="{Binding HasDlc}"
|
||||
Header="{ext:Locale GameListContextMenuExtractDataAocRomFS}"
|
||||
ToolTip.Tip="{ext:Locale GameListContextMenuExtractDataAocRomFSToolTip}" />
|
||||
<MenuItem
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
NavigationDialogHost content = new(ownerAccountManager, ownerContentManager, ownerVirtualFileSystem, ownerHorizonClient);
|
||||
ContentDialog contentDialog = new()
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle],
|
||||
Title = LocaleManager.Instance[LocaleKeys.UserProfiles_WindowTitle],
|
||||
PrimaryButtonText = string.Empty,
|
||||
SecondaryButtonText = string.Empty,
|
||||
CloseButtonText = string.Empty,
|
||||
@@ -156,7 +156,7 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
{
|
||||
_ = Dispatcher.UIThread.InvokeAsync(async ()
|
||||
=> await ContentDialogHelper.CreateErrorDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionWarningMessage]));
|
||||
LocaleManager.Instance[LocaleKeys.UserProfiles_DialogUserProfileDeletionWarningMessage]));
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -165,8 +165,8 @@ namespace Ryujinx.Ava.UI.Controls
|
||||
}
|
||||
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUserProfileDeletionConfirmMessage],
|
||||
string.Empty,
|
||||
LocaleManager.Instance[LocaleKeys.UserProfiles_DialogUserProfileDeletionConfirmMessage],
|
||||
LocaleManager.Instance[LocaleKeys.IrreversibleActionNote],
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||
string.Empty);
|
||||
|
||||
@@ -10,6 +10,9 @@ namespace Ryujinx.Ava.UI.Models
|
||||
[ObservableProperty]
|
||||
public partial byte[] Image { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool FirmwareFound { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string Name { get; set; } = string.Empty;
|
||||
|
||||
|
||||
@@ -2,8 +2,12 @@ using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform;
|
||||
using Ryujinx.Ava.Systems.Configuration;
|
||||
using Ryujinx.Ava.Utilities;
|
||||
using Ryujinx.Common.Configuration;
|
||||
using Ryujinx.Common.Helper;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.RenderDocApi;
|
||||
using Ryujinx.HLE;
|
||||
using SPB.Graphics;
|
||||
using SPB.Platform;
|
||||
using SPB.Platform.GLX;
|
||||
@@ -30,6 +34,7 @@ namespace Ryujinx.Ava.UI.Renderer
|
||||
protected nint MetalLayer { get; set; }
|
||||
|
||||
public delegate void UpdateBoundsCallbackDelegate(Rect rect);
|
||||
|
||||
private UpdateBoundsCallbackDelegate _updateBoundsCallback;
|
||||
|
||||
public event EventHandler<nint> WindowCreated;
|
||||
@@ -46,6 +51,55 @@ namespace Ryujinx.Ava.UI.Renderer
|
||||
|
||||
protected virtual void OnWindowDestroyed() { }
|
||||
|
||||
public bool ToggleRenderDocCapture(Switch device)
|
||||
{
|
||||
if (!RenderDoc.IsAvailable) return false;
|
||||
|
||||
if (RenderDoc.IsFrameCapturing)
|
||||
{
|
||||
if (EndRenderDocCapture())
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, "Ended RenderDoc capture.");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (StartRenderDocCapture(device))
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, "Starting RenderDoc capture.");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool StartRenderDocCapture(Switch device)
|
||||
{
|
||||
if (!RenderDoc.IsAvailable) return false;
|
||||
|
||||
if (RenderDoc.IsFrameCapturing) return false;
|
||||
|
||||
RenderDoc.StartFrameCapture(nint.Zero, WindowHandle);
|
||||
RenderDoc.SetCaptureTitle(TitleHelper.FormatRenderDocCaptureTitle(device.Processes.ActiveApplication, Program.Version));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool EndRenderDocCapture()
|
||||
{
|
||||
if (!RenderDoc.IsAvailable) return false;
|
||||
if (!RenderDoc.IsFrameCapturing) return false;
|
||||
|
||||
return RenderDoc.IsFrameCapturing && RenderDoc.EndFrameCapture(nint.Zero, WindowHandle);
|
||||
}
|
||||
|
||||
public bool DiscardRenderDocCapture()
|
||||
{
|
||||
if (!RenderDoc.IsAvailable) return false;
|
||||
if (!RenderDoc.IsFrameCapturing) return false;
|
||||
|
||||
return RenderDoc.IsFrameCapturing && RenderDoc.DiscardFrameCapture(nint.Zero, WindowHandle);
|
||||
}
|
||||
|
||||
protected virtual void OnWindowDestroying()
|
||||
{
|
||||
WindowHandle = nint.Zero;
|
||||
@@ -124,7 +178,9 @@ namespace Ryujinx.Ava.UI.Renderer
|
||||
}
|
||||
else
|
||||
{
|
||||
X11Window = PlatformHelper.CreateOpenGLWindow(new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false), 0, 0, 100, 100) as GLXWindow;
|
||||
X11Window = PlatformHelper.CreateOpenGLWindow(
|
||||
new FramebufferFormat(new ColorFormat(8, 8, 8, 0), 16, 0, ColorFormat.Zero, 0, 2, false), 0, 0, 100,
|
||||
100) as GLXWindow;
|
||||
}
|
||||
|
||||
WindowHandle = X11Window.WindowHandle.RawHandle;
|
||||
@@ -138,7 +194,7 @@ namespace Ryujinx.Ava.UI.Renderer
|
||||
{
|
||||
_className = "NativeWindow-" + Guid.NewGuid();
|
||||
|
||||
_wndProcDelegate = delegate (nint hWnd, WindowsMessages msg, nint wParam, nint lParam)
|
||||
_wndProcDelegate = delegate(nint hWnd, WindowsMessages msg, nint wParam, nint lParam)
|
||||
{
|
||||
switch (msg)
|
||||
{
|
||||
@@ -161,7 +217,8 @@ namespace Ryujinx.Ava.UI.Renderer
|
||||
|
||||
RegisterClassEx(ref wndClassEx);
|
||||
|
||||
WindowHandle = CreateWindowEx(0, _className, "NativeWindow", WindowStyles.WsChild, 0, 0, 640, 480, control.Handle, nint.Zero, nint.Zero, nint.Zero);
|
||||
WindowHandle = CreateWindowEx(0, _className, "NativeWindow", WindowStyles.WsChild, 0, 0, 640, 480,
|
||||
control.Handle, nint.Zero, nint.Zero, nint.Zero);
|
||||
|
||||
SetWindowLongPtrW(control.Handle, GWLP_WNDPROC, wndClassEx.lpfnWndProc);
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ using Ryujinx.Ava.UI.Windows;
|
||||
using Ryujinx.Ava.Utilities;
|
||||
using Ryujinx.Common;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Graphics.RenderDocApi;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
@@ -56,6 +57,8 @@ namespace Ryujinx.Ava
|
||||
|
||||
if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
// Switches macOS key held behavior to repeat the input key instead of showing the character accents menu (like doing on an iOS keyboard would).
|
||||
// https://macos-defaults.com/keyboard/applepressandholdenabled.html
|
||||
Process.Start("/usr/bin/defaults", "write org.ryujinx.Ryujinx ApplePressAndHoldEnabled -bool false");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +184,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
_controller = 0;
|
||||
}
|
||||
|
||||
if (Controllers.Count > 0 && value < Controllers.Count && _controller > -1)
|
||||
if (Controllers.Count > 0 && _controller < Controllers.Count && _controller > -1)
|
||||
{
|
||||
ControllerType controller = Controllers[_controller].Type;
|
||||
|
||||
@@ -467,7 +467,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
IsModified = true;
|
||||
RevertChanges();
|
||||
FindPairedDeviceInConfigFile();
|
||||
|
||||
|
||||
_isChangeTrackingActive = true; // Enable configuration change tracking
|
||||
|
||||
}
|
||||
@@ -521,7 +521,17 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
|
||||
if (Config != null && Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType) != -1)
|
||||
{
|
||||
Controller = Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType);
|
||||
int controllerIndex = Controllers.ToList().FindIndex(x => x.Type == Config.ControllerType);
|
||||
|
||||
// Avalonia bug: setting a newly instanced ComboBox to 0
|
||||
// causes the selected item to show up blank
|
||||
// Workaround: set the box to 1 and then 0
|
||||
if (controllerIndex == 0)
|
||||
{
|
||||
Controller = 1;
|
||||
}
|
||||
|
||||
Controller = controllerIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -576,7 +586,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
DeviceList.Clear();
|
||||
Devices.Add((DeviceType.None, Disabled, LocaleManager.Instance[LocaleKeys.ControllerSettingsDeviceDisabled]));
|
||||
|
||||
int controllerNumber = 0;
|
||||
|
||||
foreach (string id in _mainWindow.InputManager.KeyboardDriver.GamepadsIds)
|
||||
{
|
||||
using IGamepad gamepad = _mainWindow.InputManager.KeyboardDriver.GetGamepad(id);
|
||||
@@ -593,6 +603,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
|
||||
if (gamepad != null)
|
||||
{
|
||||
int controllerNumber = 0;
|
||||
string name = GetUniqueGamepadName(gamepad, ref controllerNumber);
|
||||
Devices.Add((DeviceType.Controller, id, name));
|
||||
}
|
||||
@@ -950,8 +961,10 @@ namespace Ryujinx.Ava.UI.ViewModels.Input
|
||||
LoadConfiguration(); // configuration preload is required if the paired gamepad was disconnected but was changed to another gamepad
|
||||
Device = Devices.ToList().FindIndex(d => d.Id == RevertDeviceId);
|
||||
|
||||
LoadDevice();
|
||||
_isLoaded = false;
|
||||
LoadConfiguration();
|
||||
LoadDevice();
|
||||
_isLoaded = true;
|
||||
|
||||
OnPropertyChanged();
|
||||
IsModified = false;
|
||||
|
||||
@@ -38,6 +38,7 @@ using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Common.UI;
|
||||
using Ryujinx.Common.Utilities;
|
||||
using Ryujinx.Cpu;
|
||||
using Ryujinx.Graphics.RenderDocApi;
|
||||
using Ryujinx.HLE;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.HLE.HOS;
|
||||
@@ -104,7 +105,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
[ObservableProperty] public partial Brush ProgressBarForegroundColor { get; set; }
|
||||
|
||||
[ObservableProperty] public partial Brush ProgressBarBackgroundColor { get; set; }
|
||||
|
||||
|
||||
#pragma warning disable MVVMTK0042 // Must stay a normal observable field declaration since this is used as an out parameter target
|
||||
[ObservableProperty] private ReadOnlyObservableCollection<ApplicationData> _appsObservableList;
|
||||
#pragma warning restore MVVMTK0042
|
||||
@@ -129,8 +130,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
[ObservableProperty] public partial string LastScannedAmiiboId { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial long LastFullscreenToggle { get; set; } = Environment.TickCount64;
|
||||
[ObservableProperty] public partial long LastFullscreenToggle { get; set; } = Environment.TickCount64;
|
||||
[ObservableProperty] public partial bool ShowContent { get; set; } = true;
|
||||
|
||||
[ObservableProperty] public partial float VolumeBeforeMute { get; set; }
|
||||
@@ -1865,6 +1865,29 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public void ReloadRenderDocApi()
|
||||
{
|
||||
RenderDoc.ReloadApi(ignoreAlreadyLoaded: true);
|
||||
|
||||
OnPropertiesChanged(nameof(ShowStartCaptureButton), nameof(ShowEndCaptureButton), nameof(RenderDocIsAvailable));
|
||||
|
||||
if (RenderDoc.IsAvailable)
|
||||
RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
|
||||
|
||||
NotificationHelper.ShowInformation(
|
||||
"RenderDoc API reloaded",
|
||||
RenderDoc.IsAvailable ? "RenderDoc is now available." : "RenderDoc is no longer available."
|
||||
);
|
||||
}
|
||||
|
||||
public void ToggleCapture()
|
||||
{
|
||||
if (ShowLoadProgress) return;
|
||||
|
||||
AppHost.RendererHost.EmbeddedWindow.ToggleRenderDocCapture(AppHost.Device);
|
||||
RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
|
||||
}
|
||||
|
||||
public void ToggleFullscreen()
|
||||
{
|
||||
if (Environment.TickCount64 - LastFullscreenToggle < HotKeyPressDelayMs)
|
||||
@@ -1955,7 +1978,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
if (ConfigurationState.Instance.Debug.EnableGdbStub)
|
||||
{
|
||||
NotificationHelper.ShowInformation(
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.NotificationLaunchCheckGdbStubTitle, ConfigurationState.Instance.Debug.GdbStubPort.Value),
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.NotificationLaunchCheckGdbStubTitle,
|
||||
ConfigurationState.Instance.Debug.GdbStubPort.Value),
|
||||
LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckGdbStubMessage]);
|
||||
}
|
||||
|
||||
@@ -1964,10 +1988,12 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
var memoryConfigurationLocaleKey = ConfigurationState.Instance.System.DramSize.Value switch
|
||||
{
|
||||
MemoryConfiguration.MemoryConfiguration4GiB or
|
||||
MemoryConfiguration.MemoryConfiguration4GiBAppletDev or
|
||||
MemoryConfiguration.MemoryConfiguration4GiBSystemDev => LocaleKeys.SettingsTabSystemDramSize4GiB,
|
||||
MemoryConfiguration.MemoryConfiguration4GiBAppletDev or
|
||||
MemoryConfiguration.MemoryConfiguration4GiBSystemDev =>
|
||||
LocaleKeys.SettingsTabSystemDramSize4GiB,
|
||||
MemoryConfiguration.MemoryConfiguration6GiB or
|
||||
MemoryConfiguration.MemoryConfiguration6GiBAppletDev => LocaleKeys.SettingsTabSystemDramSize6GiB,
|
||||
MemoryConfiguration.MemoryConfiguration6GiBAppletDev =>
|
||||
LocaleKeys.SettingsTabSystemDramSize6GiB,
|
||||
MemoryConfiguration.MemoryConfiguration8GiB => LocaleKeys.SettingsTabSystemDramSize8GiB,
|
||||
MemoryConfiguration.MemoryConfiguration12GiB => LocaleKeys.SettingsTabSystemDramSize12GiB,
|
||||
_ => LocaleKeys.SettingsTabSystemDramSize4GiB,
|
||||
@@ -1975,9 +2001,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
NotificationHelper.ShowWarning(
|
||||
LocaleManager.Instance.UpdateAndGetDynamicValue(
|
||||
LocaleKeys.NotificationLaunchCheckDramSizeTitle,
|
||||
LocaleKeys.NotificationLaunchCheckDramSizeTitle,
|
||||
LocaleManager.Instance[memoryConfigurationLocaleKey]
|
||||
),
|
||||
),
|
||||
LocaleManager.Instance[LocaleKeys.NotificationLaunchCheckDramSizeMessage]);
|
||||
}
|
||||
}
|
||||
@@ -2091,6 +2117,8 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
}
|
||||
);
|
||||
|
||||
public string FavoriteStatusText => SelectedApplication?.Favorite == false ? LocaleManager.Instance[LocaleKeys.GameListContextMenuAddToFavorites] : LocaleManager.Instance[LocaleKeys.GameListContextMenuRemoveFromFavorites];
|
||||
|
||||
public static RelayCommand<MainWindowViewModel> CreateApplicationShortcut { get; } =
|
||||
Commands.CreateConditional<MainWindowViewModel>(vm => vm?.SelectedApplication != null,
|
||||
viewModel => ShortcutHelper.CreateAppShortcut(
|
||||
@@ -2462,6 +2490,67 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
png.SaveTo(fileStream);
|
||||
});
|
||||
|
||||
public bool ShowStartCaptureButton => !RenderDocIsCapturing && RenderDoc.IsAvailable;
|
||||
public bool ShowEndCaptureButton => RenderDocIsCapturing && RenderDoc.IsAvailable;
|
||||
public static bool RenderDocIsAvailable => RenderDoc.IsAvailable;
|
||||
|
||||
public bool RenderDocIsCapturing
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged();
|
||||
OnPropertiesChanged(nameof(ShowStartCaptureButton), nameof(ShowEndCaptureButton));
|
||||
}
|
||||
}
|
||||
|
||||
public static RelayCommand<MainWindowViewModel> StartRenderDocCapture { get; } =
|
||||
Commands.CreateConditional<MainWindowViewModel>(vm => RenderDoc.IsAvailable && !vm.ShowLoadProgress,
|
||||
viewModel =>
|
||||
{
|
||||
if (!RenderDoc.IsFrameCapturing)
|
||||
{
|
||||
if (viewModel.AppHost.RendererHost
|
||||
.EmbeddedWindow.StartRenderDocCapture(viewModel.AppHost.Device))
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, "Starting RenderDoc capture.");
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
|
||||
});
|
||||
|
||||
public static RelayCommand<MainWindowViewModel> EndRenderDocCapture { get; } =
|
||||
Commands.CreateConditional<MainWindowViewModel>(vm => RenderDoc.IsAvailable && !vm.ShowLoadProgress,
|
||||
viewModel =>
|
||||
{
|
||||
if (RenderDoc.IsFrameCapturing)
|
||||
{
|
||||
if (viewModel.AppHost.RendererHost.EmbeddedWindow.EndRenderDocCapture())
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, "Ended RenderDoc capture.");
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
|
||||
});
|
||||
|
||||
public static RelayCommand<MainWindowViewModel> DiscardRenderDocCapture { get; } =
|
||||
Commands.CreateConditional<MainWindowViewModel>(vm => RenderDoc.IsAvailable && !vm.ShowLoadProgress,
|
||||
viewModel =>
|
||||
{
|
||||
if (RenderDoc.IsFrameCapturing)
|
||||
{
|
||||
if (viewModel.AppHost.RendererHost.EmbeddedWindow.DiscardRenderDocCapture())
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, "Discarded RenderDoc capture.");
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.RenderDocIsCapturing = RenderDoc.IsFrameCapturing;
|
||||
});
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using LibHac.Tools.FsSystem;
|
||||
using Ryujinx.Audio.Backends.Apple;
|
||||
using Ryujinx.Audio.Backends.OpenAL;
|
||||
using Ryujinx.Audio.Backends.SDL3;
|
||||
using Ryujinx.Audio.Backends.SoundIo;
|
||||
@@ -277,6 +278,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
public bool IsOpenAlEnabled { get; set; }
|
||||
public bool IsSoundIoEnabled { get; set; }
|
||||
public bool IsSDL3Enabled { get; set; }
|
||||
public bool IsAudioToolboxEnabled { get; set; }
|
||||
public bool IsCustomResolutionScaleActive => _resolutionScale == 4;
|
||||
public bool IsScalingFilterActive => _scalingFilter == (int)Ryujinx.Common.Configuration.ScalingFilter.Fsr;
|
||||
|
||||
@@ -524,12 +526,14 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
IsOpenAlEnabled = OpenALHardwareDeviceDriver.IsSupported;
|
||||
IsSoundIoEnabled = SoundIoHardwareDeviceDriver.IsSupported;
|
||||
IsSDL3Enabled = SDL3HardwareDeviceDriver.IsSupported;
|
||||
IsAudioToolboxEnabled = OperatingSystem.IsMacOS() && AppleHardwareDeviceDriver.IsSupported;
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
OnPropertyChanged(nameof(IsOpenAlEnabled));
|
||||
OnPropertyChanged(nameof(IsSoundIoEnabled));
|
||||
OnPropertyChanged(nameof(IsSDL3Enabled));
|
||||
OnPropertyChanged(nameof(IsAudioToolboxEnabled));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,9 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ProfileImageModel> Images { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool FirmwareFound { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial Color BackgroundColor { get; set; } = Colors.White;
|
||||
|
||||
@@ -43,17 +46,15 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
};
|
||||
}
|
||||
|
||||
private int _selectedIndex = -1;
|
||||
|
||||
public int SelectedIndex
|
||||
{
|
||||
get;
|
||||
get => _selectedIndex;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
|
||||
SelectedImage = field == -1
|
||||
? null
|
||||
: Images[field].Data;
|
||||
|
||||
_selectedIndex = value;
|
||||
SelectedImage = value == -1 ? null : Images[value].Data;
|
||||
OnPropertyChanged();
|
||||
OnPropertyChanged(nameof(SelectedImage));
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
public partial class UserProfileImageSelectorViewModel : BaseModel
|
||||
{
|
||||
[ObservableProperty]
|
||||
public partial bool FirmwareFound { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Collections.Specialized;
|
||||
|
||||
namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
@@ -9,20 +9,35 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
{
|
||||
public UserProfileViewModel()
|
||||
{
|
||||
Profiles = [];
|
||||
LostProfiles = [];
|
||||
IsEmpty = !LostProfiles.Any();
|
||||
Profiles = new ObservableCollection<BaseModel>();
|
||||
LostProfiles = new ObservableCollection<UserProfile>();
|
||||
LostProfiles.CollectionChanged += LostProfilesChanged;
|
||||
}
|
||||
|
||||
public ObservableCollection<BaseModel> Profiles { get; set; }
|
||||
public ObservableCollection<BaseModel> Profiles { get; }
|
||||
|
||||
public ObservableCollection<UserProfile> LostProfiles { get; set; }
|
||||
public ObservableCollection<UserProfile> LostProfiles { get; }
|
||||
|
||||
public bool IsEmpty { get; set; }
|
||||
public bool IsEmpty => LostProfiles.Count == 0;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
LostProfiles.CollectionChanged -= LostProfilesChanged;
|
||||
}
|
||||
|
||||
private void LostProfilesChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
OnPropertyChanged(nameof(IsEmpty));
|
||||
}
|
||||
|
||||
public void UpdateLostProfiles(ObservableCollection<UserProfile> newProfiles)
|
||||
{
|
||||
LostProfiles.Clear();
|
||||
|
||||
foreach (var profile in newProfiles)
|
||||
LostProfiles.Add(profile);
|
||||
|
||||
OnPropertyChanged(nameof(IsEmpty));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace Ryujinx.Ava.UI.ViewModels
|
||||
|
||||
private readonly AccountManager _accountManager;
|
||||
|
||||
public string SaveManagerHeading => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.SaveManagerHeading, _accountManager.LastOpenedUser.Name, _accountManager.LastOpenedUser.UserId);
|
||||
public string SaveManagerTitle => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.UserProfiles_SaveManagerTitle, _accountManager.LastOpenedUser.Name);
|
||||
|
||||
public UserSaveManagerViewModel(AccountManager accountManager)
|
||||
{
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace Ryujinx.Ava.UI.Views.Dialog
|
||||
{
|
||||
PrimaryButtonText = string.Empty,
|
||||
SecondaryButtonText = string.Empty,
|
||||
CloseButtonText = LocaleManager.Instance[LocaleKeys.UserProfilesClose],
|
||||
CloseButtonText = LocaleManager.Instance[LocaleKeys.SettingsButtonClose],
|
||||
Content = new AboutView { ViewModel = viewModel }
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
|
||||
xmlns:common="clr-namespace:Ryujinx.Common;assembly=Ryujinx.Common"
|
||||
xmlns:renderDocApi="clr-namespace:Ryujinx.Graphics.RenderDocApi;assembly=Ryujinx.Graphics.RenderDocApi"
|
||||
x:DataType="viewModels:MainWindowViewModel"
|
||||
x:Class="Ryujinx.Ava.UI.Views.Main.MainMenuBarView">
|
||||
<Design.DataContext>
|
||||
@@ -135,7 +136,7 @@
|
||||
<MenuItem
|
||||
Command="{Binding ManageProfiles}"
|
||||
Padding="0"
|
||||
Header="{ext:Locale MenuBarOptionsManageUserProfiles}"
|
||||
Header="{ext:Locale UserProfiles_MenuBarOptions_OpenUserProfiles}"
|
||||
Icon="{ext:Icon fa-solid fa-user}"
|
||||
IsEnabled="{Binding EnableNonGameRunningControls}"
|
||||
Classes="withCheckbox">
|
||||
@@ -200,6 +201,29 @@
|
||||
Header="{ext:Locale GameListContextMenuManageCheat}"
|
||||
Icon="{ext:Icon fa-solid fa-code}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
<Separator IsVisible="{Binding RenderDocIsAvailable}" />
|
||||
<MenuItem
|
||||
IsVisible="{Binding ShowStartCaptureButton}"
|
||||
Command="{Binding StartRenderDocCapture}"
|
||||
CommandParameter="{Binding}"
|
||||
Header="{ext:Locale RenderDoc_MenuBarActions_StartCapture}"
|
||||
Icon="{ext:Icon fa-solid fa-video}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
<MenuItem
|
||||
IsVisible="{Binding ShowEndCaptureButton}"
|
||||
Command="{Binding EndRenderDocCapture}"
|
||||
CommandParameter="{Binding}"
|
||||
Header="{ext:Locale RenderDoc_MenuBarActions_EndCapture}"
|
||||
Icon="{ext:Icon fa-solid fa-video-slash}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
<MenuItem
|
||||
IsVisible="{Binding ShowEndCaptureButton}"
|
||||
Command="{Binding DiscardRenderDocCapture}"
|
||||
CommandParameter="{Binding}"
|
||||
Header="{ext:Locale RenderDoc_MenuBarActions_DiscardCapture}"
|
||||
ToolTip.Tip="{ext:Locale RenderDoc_MenuBarActions_DiscardCapture_ToolTip}"
|
||||
Icon="{ext:Icon fa-solid fa-video-slash}"
|
||||
IsEnabled="{Binding IsGameRunning}" />
|
||||
</MenuItem>
|
||||
<MenuItem VerticalAlignment="Center" Header="{ext:Locale MenuBarActions}" IsVisible="{Binding EnableNonGameRunningControls}">
|
||||
<MenuItem Header="{ext:Locale MenuBarActionsInstallKeys}" Icon="{ext:Icon fa-solid fa-key}">
|
||||
|
||||
@@ -85,37 +85,24 @@ namespace Ryujinx.Ava.UI.Views.Main
|
||||
|
||||
private static IEnumerable<MenuItem> GenerateLanguageMenuItems()
|
||||
{
|
||||
const string LocalePath = "Ryujinx/Assets/Locale.json";
|
||||
const string LanguagesPath = "Ryujinx/Assets/Languages.json";
|
||||
|
||||
string languageJson = EmbeddedResources.ReadAllText(LocalePath);
|
||||
string languageJson = EmbeddedResources.ReadAllText(LanguagesPath);
|
||||
string currentLanguageCode = LocaleManager.Instance.CurrentLanguageCode;
|
||||
|
||||
LocalesJson locales = JsonHelper.Deserialize(languageJson, LocalesJsonContext.Default.LocalesJson);
|
||||
LanguagesJson languages = JsonHelper.Deserialize(languageJson, LanguagesJsonContext.Default.LanguagesJson);
|
||||
|
||||
foreach (string language in locales.Languages)
|
||||
foreach ((string code, string language) in languages.Languages)
|
||||
{
|
||||
int index = locales.Locales.FindIndex(x => x.ID == "Language");
|
||||
string languageName;
|
||||
|
||||
if (index == -1)
|
||||
{
|
||||
languageName = language;
|
||||
}
|
||||
else
|
||||
{
|
||||
string tr = locales.Locales[index].Translations[language];
|
||||
languageName = string.IsNullOrEmpty(tr)
|
||||
? language
|
||||
: tr;
|
||||
}
|
||||
string languageName = string.IsNullOrEmpty(language) ? code : language;
|
||||
|
||||
MenuItem menuItem = new()
|
||||
{
|
||||
Padding = new Thickness(15, 0, 0, 0),
|
||||
Margin = new Thickness(3, 0, 3, 0),
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch,
|
||||
Header = language == currentLanguageCode ? $"{languageName} ✔" : languageName,
|
||||
Command = Commands.Create(() => MainWindowViewModel.ChangeLanguage(language))
|
||||
Header = code == currentLanguageCode ? $"{languageName} ✔" : languageName,
|
||||
Command = Commands.Create(() => MainWindowViewModel.ChangeLanguage(code))
|
||||
};
|
||||
|
||||
yield return menuItem;
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
<ComboBoxItem
|
||||
IsEnabled="{Binding IsSDL3Enabled}"
|
||||
Content="{ext:Locale SettingsTabSystemAudioBackendSDL3}" />
|
||||
<ComboBoxItem
|
||||
IsEnabled="{Binding IsAudioToolboxEnabled}"
|
||||
Content="{ext:Locale SettingsTabSystemAudioBackendAudioToolbox}" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="10,0,0,0" Orientation="Horizontal">
|
||||
|
||||
@@ -8,107 +8,102 @@
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
|
||||
Margin="0"
|
||||
MinWidth="500"
|
||||
Padding="0"
|
||||
mc:Ignorable="d"
|
||||
MinWidth="500"
|
||||
Focusable="True"
|
||||
x:DataType="models:TempProfile">
|
||||
<Grid Margin="0" ColumnDefinitions="Auto,*" RowDefinitions="*,Auto">
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Orientation="Vertical"
|
||||
Spacing="10">
|
||||
<TextBlock Text="{ext:Locale UserProfilesName}" />
|
||||
<Grid Margin="0,10,0,0" ColumnDefinitions="Auto,*" RowDefinitions="*,Auto">
|
||||
<StackPanel Spacing="10">
|
||||
<TextBlock Text="{ext:Locale UserProfiles_NameLabel}" />
|
||||
<TextBox
|
||||
Name="NameBox"
|
||||
Margin="0,0,0,5"
|
||||
Width="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
MaxLength="{Binding MaxProfileNameLength}"
|
||||
Watermark="{ext:Locale ProfileNameSelectionWatermark}"
|
||||
Watermark="{ext:Locale UserProfiles_ProfileNameSelectionWatermark}"
|
||||
Text="{Binding Name}" />
|
||||
<TextBlock Name="IdText" Text="{ext:Locale UserProfilesUserId}" />
|
||||
<TextBlock Name="IdText" Text="{ext:Locale UserProfiles_UserIdLabel}" />
|
||||
<TextBox
|
||||
Name="IdLabel"
|
||||
Width="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
IsReadOnly="True"
|
||||
Text="{Binding UserIdString}" />
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Stretch"
|
||||
Orientation="Vertical">
|
||||
<Grid Grid.Column="1">
|
||||
<Border
|
||||
Name="ImageBox"
|
||||
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
|
||||
BorderThickness="1">
|
||||
BorderThickness="1"
|
||||
VerticalAlignment="Bottom"
|
||||
HorizontalAlignment="Right">
|
||||
<Panel>
|
||||
<ui:SymbolIcon
|
||||
FontSize="60"
|
||||
Width="96"
|
||||
Height="96"
|
||||
Margin="0"
|
||||
FontSize="70"
|
||||
Width="120"
|
||||
Height="120"
|
||||
Foreground="{DynamicResource AppListHoverBackgroundColor}"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
Symbol="Camera" />
|
||||
<Image
|
||||
Name="ProfileImage"
|
||||
Width="96"
|
||||
Height="96"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
Width="120"
|
||||
Height="120"
|
||||
Source="{Binding Image, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
|
||||
<Border
|
||||
Margin="2"
|
||||
Height="27"
|
||||
Width="27"
|
||||
CornerRadius="17"
|
||||
VerticalAlignment="Top"
|
||||
HorizontalAlignment="Right"
|
||||
Background="{DynamicResource ThemeContentBackgroundColor}">
|
||||
<Button
|
||||
Name="ProfileImageButton"
|
||||
MaxHeight="27"
|
||||
MaxWidth="27"
|
||||
MinWidth="27"
|
||||
MinHeight="27"
|
||||
CornerRadius="17"
|
||||
Padding="0">
|
||||
<ui:SymbolIcon Symbol="Edit" />
|
||||
<Button.Flyout>
|
||||
<MenuFlyout Placement="Bottom">
|
||||
<MenuItem
|
||||
Header="{ext:Locale UserProfiles_ProfileImage_Import}"
|
||||
Icon="{ext:Icon fa-solid fa-image}"
|
||||
Click="Import_OnClick" />
|
||||
<MenuItem
|
||||
Header="{ext:Locale UserProfiles_ProfileImage_SelectAvatar}"
|
||||
Icon="{ext:Icon fa-solid fa-floppy-disk}"
|
||||
Click="SelectFirmwareImage_OnClick" />
|
||||
</MenuFlyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
</Border>
|
||||
</Panel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
HorizontalAlignment="Left"
|
||||
Orientation="Horizontal"
|
||||
Margin="0 24 0 0"
|
||||
Margin="0,30,0,0"
|
||||
Spacing="10">
|
||||
<Button
|
||||
Width="50"
|
||||
MinWidth="50"
|
||||
Click="BackButton_Click">
|
||||
<Button MinWidth="50" Width="50" Click="BackButton_Click">
|
||||
<ui:SymbolIcon Symbol="Back" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal"
|
||||
Margin="0 24 0 0"
|
||||
Margin="0,30,0,0"
|
||||
Spacing="10">
|
||||
<Button
|
||||
Name="DeleteButton"
|
||||
Click="DeleteButton_Click">
|
||||
<TextBlock Text="{ext:Locale UserProfilesDelete}" />
|
||||
<Button Name="DeleteButton" Click="DeleteButton_Click">
|
||||
<TextBlock Text="{ext:Locale UserProfiles_ButtonDelete}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="ChangePictureButton"
|
||||
Click="ChangePictureButton_Click">
|
||||
<TextBlock Text="{ext:Locale UserProfilesChangeProfileImage}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="AddPictureButton"
|
||||
Click="ChangePictureButton_Click">
|
||||
<TextBlock Text="{ext:Locale UserProfilesSetProfileImage}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="SaveButton"
|
||||
Click="SaveButton_Click">
|
||||
<TextBlock Text="{ext:Locale UserProfilesSetProfileImage}" />
|
||||
<Button Name="SaveButton" Click="SaveButton_Click">
|
||||
<TextBlock Text="{ext:Locale UserProfiles_ButtonSave}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Data;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Markup.Xaml.MarkupExtensions;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.VisualTree;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using FluentAvalonia.UI.Navigation;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
@@ -8,6 +12,10 @@ using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Helpers;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.HLE.HOS.Services.Account.Acc;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using SkiaSharp;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using UserProfile = Ryujinx.Ava.UI.Models.UserProfile;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.User
|
||||
@@ -15,90 +23,75 @@ namespace Ryujinx.Ava.UI.Views.User
|
||||
public partial class UserEditorView : RyujinxControl<TempProfile>
|
||||
{
|
||||
private NavigationDialogHost _parent;
|
||||
private ContentManager _contentManager;
|
||||
private UserProfile _profile;
|
||||
private TempProfile _tempProfile;
|
||||
private bool _isNewUser;
|
||||
|
||||
public static uint MaxProfileNameLength => 0x20;
|
||||
public bool IsDeletable => _profile.UserId != AccountManager.DefaultUserId;
|
||||
|
||||
public string UserEditorTitle => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.UserProfiles_UserEditorTitle, _profile.Name);
|
||||
public UserEditorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
||||
{
|
||||
NavigatedTo(e);
|
||||
}, RoutingStrategies.Direct);
|
||||
AddHandler(Frame.NavigatedToEvent, (s, e) => NavigatedTo(e), RoutingStrategies.Direct);
|
||||
}
|
||||
|
||||
private void NavigatedTo(NavigationEventArgs arg)
|
||||
{
|
||||
if (Program.PreviewerDetached)
|
||||
if (!Program.PreviewerDetached)
|
||||
return;
|
||||
|
||||
if (arg.NavigationMode == NavigationMode.New)
|
||||
{
|
||||
switch (arg.NavigationMode)
|
||||
{
|
||||
case NavigationMode.New:
|
||||
(NavigationDialogHost parent, UserProfile profile, bool isNewUser) = ((NavigationDialogHost parent, UserProfile profile, bool isNewUser))arg.Parameter;
|
||||
_isNewUser = isNewUser;
|
||||
_profile = profile;
|
||||
ViewModel = new TempProfile(_profile);
|
||||
(NavigationDialogHost parent, UserProfile profile, bool isNewUser) =
|
||||
((NavigationDialogHost parent, UserProfile profile, bool isNewUser))arg.Parameter;
|
||||
|
||||
_parent = parent;
|
||||
break;
|
||||
}
|
||||
_parent = parent;
|
||||
_profile = profile;
|
||||
_isNewUser = isNewUser;
|
||||
|
||||
((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - " +
|
||||
$"{(_isNewUser ? LocaleManager.Instance[LocaleKeys.UserEditorTitleCreate] : LocaleManager.Instance[LocaleKeys.UserEditorTitle])}";
|
||||
DataValidationErrors.ClearErrors(NameBox);
|
||||
DataValidationErrors.ClearErrors(ImageBox);
|
||||
ImageBox.Bind(Border.BorderBrushProperty, new DynamicResourceExtension("AppListHoverBackgroundColor"));
|
||||
|
||||
AddPictureButton.IsVisible = _isNewUser;
|
||||
ChangePictureButton.IsVisible = !_isNewUser;
|
||||
IdLabel.IsVisible = _profile != null;
|
||||
IdText.IsVisible = _profile != null;
|
||||
if (!_isNewUser && IsDeletable)
|
||||
{
|
||||
DeleteButton.IsVisible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
DeleteButton.IsVisible = false;
|
||||
}
|
||||
ViewModel = new TempProfile(_profile);
|
||||
_tempProfile = ViewModel;
|
||||
|
||||
_contentManager = _parent.ContentManager;
|
||||
ViewModel.FirmwareFound = _contentManager.GetCurrentFirmwareVersion() != null;
|
||||
}
|
||||
|
||||
((ContentDialog)_parent.Parent).Title =
|
||||
$"{LocaleManager.Instance[LocaleKeys.UserProfiles_WindowTitle]} - " +
|
||||
$"{(_isNewUser ? LocaleManager.Instance[LocaleKeys.UserProfiles_UserEditorTitleNewUser] : UserEditorTitle)}";
|
||||
|
||||
bool hasProfile = _profile != null;
|
||||
IdLabel.IsVisible = hasProfile;
|
||||
IdText.IsVisible = hasProfile;
|
||||
|
||||
DeleteButton.IsVisible = !_isNewUser && IsDeletable;
|
||||
}
|
||||
|
||||
private async void BackButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_isNewUser)
|
||||
bool hasUnsavedChanges =
|
||||
_isNewUser
|
||||
? (ViewModel.Name != string.Empty || ViewModel.Image != null)
|
||||
: (_profile.Name != ViewModel.Name || _profile.Image != ViewModel.Image);
|
||||
|
||||
if (hasUnsavedChanges)
|
||||
{
|
||||
if (ViewModel.Name != string.Empty || ViewModel.Image != null)
|
||||
{
|
||||
if (await ContentDialogHelper.CreateChoiceDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesTitle],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesMessage],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesSubMessage]))
|
||||
{
|
||||
_parent?.GoBack();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bool confirm = await ContentDialogHelper.CreateChoiceDialog(
|
||||
LocaleManager.Instance[LocaleKeys.UserProfiles_DialogUserProfileUnsavedChangesTitle],
|
||||
LocaleManager.Instance[LocaleKeys.UserProfiles_DialogUserProfileUnsavedChangesMessage],
|
||||
LocaleManager.Instance[LocaleKeys.UserProfiles_DialogUserProfileUnsavedChangesSubMessage]);
|
||||
|
||||
if (confirm)
|
||||
_parent?.GoBack();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_profile.Name != ViewModel.Name || _profile.Image != ViewModel.Image)
|
||||
{
|
||||
if (await ContentDialogHelper.CreateChoiceDialog(
|
||||
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesTitle],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesMessage],
|
||||
LocaleManager.Instance[LocaleKeys.DialogUserProfileUnsavedChangesSubMessage]))
|
||||
{
|
||||
_parent?.GoBack();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_parent?.GoBack();
|
||||
}
|
||||
_parent?.GoBack();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,18 +103,28 @@ namespace Ryujinx.Ava.UI.Views.User
|
||||
private void SaveButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
DataValidationErrors.ClearErrors(NameBox);
|
||||
DataValidationErrors.ClearErrors(ImageBox);
|
||||
ImageBox.Bind(Border.BorderBrushProperty, new DynamicResourceExtension("AppListHoverBackgroundColor"));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(ViewModel.Name))
|
||||
bool nameEmpty = string.IsNullOrWhiteSpace(ViewModel.Name);
|
||||
bool imageMissing = ViewModel.Image == null;
|
||||
|
||||
if (nameEmpty && imageMissing)
|
||||
{
|
||||
DataValidationErrors.SetError(NameBox, new DataValidationException(LocaleManager.Instance[LocaleKeys.UserProfiles_EmptyNameError]));
|
||||
DataValidationErrors.SetError(ImageBox, new DataValidationException(""));
|
||||
ImageBox.BorderBrush = Brush.Parse("#ff99a4");
|
||||
return;
|
||||
}
|
||||
else if (nameEmpty)
|
||||
{
|
||||
DataValidationErrors.SetError(NameBox, new DataValidationException(LocaleManager.Instance[LocaleKeys.UserProfileEmptyNameError]));
|
||||
|
||||
DataValidationErrors.SetError(NameBox,new DataValidationException(LocaleManager.Instance[LocaleKeys.UserProfiles_EmptyNameError]));
|
||||
return;
|
||||
}
|
||||
|
||||
if (ViewModel.Image == null)
|
||||
else if (imageMissing)
|
||||
{
|
||||
_parent.Navigate(typeof(UserProfileImageSelectorView), (_parent, ViewModel));
|
||||
|
||||
DataValidationErrors.SetError(ImageBox, new DataValidationException(""));
|
||||
ImageBox.BorderBrush = Brush.Parse("#ff99a4");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -130,6 +133,7 @@ namespace Ryujinx.Ava.UI.Views.User
|
||||
_profile.Name = ViewModel.Name;
|
||||
_profile.Image = ViewModel.Image;
|
||||
_profile.UpdateState();
|
||||
|
||||
_parent.AccountManager.SetUserName(_profile.UserId, _profile.Name);
|
||||
_parent.AccountManager.SetUserImage(_profile.UserId, _profile.Image);
|
||||
}
|
||||
@@ -145,17 +149,80 @@ namespace Ryujinx.Ava.UI.Views.User
|
||||
_parent?.GoBack();
|
||||
}
|
||||
|
||||
public void SelectProfileImage()
|
||||
private void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_parent.Navigate(typeof(UserProfileImageSelectorView), (_parent, ViewModel));
|
||||
}
|
||||
|
||||
private void ChangePictureButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_profile != null || _isNewUser)
|
||||
if (ViewModel.FirmwareFound)
|
||||
{
|
||||
SelectProfileImage();
|
||||
_parent.Navigate(typeof(UserFirmwareAvatarSelectorView), (_parent, _tempProfile));
|
||||
}
|
||||
}
|
||||
|
||||
private async void Import_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var window = (Window)this.GetVisualRoot()!;
|
||||
var result = await window.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = LocaleManager.Instance[LocaleKeys.UserProfiles_SupportedImageFormatDialogTitle],
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter = new List<FilePickerFileType>
|
||||
{
|
||||
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
|
||||
{
|
||||
Patterns = ["*.jpg", "*.jpeg", "*.png", "*.bmp"],
|
||||
AppleUniformTypeIdentifiers = ["public.jpeg", "public.png", "com.microsoft.bmp"],
|
||||
MimeTypes = ["image/jpeg", "image/png", "image/bmp"],
|
||||
},
|
||||
new("JPG")
|
||||
{
|
||||
Patterns = ["*.jpg"],
|
||||
AppleUniformTypeIdentifiers = ["public.jpeg"],
|
||||
MimeTypes = ["image/jpeg"],
|
||||
},
|
||||
new("JPEG")
|
||||
{
|
||||
Patterns = ["*.jpeg"],
|
||||
AppleUniformTypeIdentifiers = ["public.jpeg"],
|
||||
MimeTypes = ["image/jpeg"],
|
||||
},
|
||||
new("PNG")
|
||||
{
|
||||
Patterns = ["*.png"],
|
||||
AppleUniformTypeIdentifiers = ["public.png"],
|
||||
MimeTypes = ["image/png"],
|
||||
},
|
||||
new("BMP")
|
||||
{
|
||||
Patterns = ["*.bmp"],
|
||||
AppleUniformTypeIdentifiers = ["com.microsoft.bmp"],
|
||||
MimeTypes = ["image/bmp"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (result.Count == 0 || DataContext is not TempProfile temp)
|
||||
return;
|
||||
|
||||
temp.Image = ProcessProfileImage(File.ReadAllBytes(result[0].Path.LocalPath));
|
||||
|
||||
if (_profile != null)
|
||||
_profile.Image = temp.Image;
|
||||
}
|
||||
|
||||
private static byte[] ProcessProfileImage(byte[] buffer)
|
||||
{
|
||||
using SKBitmap bitmap = SKBitmap.Decode(buffer);
|
||||
SKBitmap resizedBitmap = bitmap.Resize(new SKImageInfo(256, 256), SKFilterQuality.High);
|
||||
|
||||
using MemoryStream streamJpg = new();
|
||||
|
||||
if (resizedBitmap != null)
|
||||
{
|
||||
using SKImage image = SKImage.FromBitmap(resizedBitmap);
|
||||
using SKData dataJpeg = image.Encode(SKEncodedImageFormat.Jpeg, 100);
|
||||
dataJpeg.SaveTo(streamJpg);
|
||||
}
|
||||
|
||||
return streamJpg.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +1,81 @@
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Views.User.UserFirmwareAvatarSelectorView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
d:DesignHeight="350"
|
||||
d:DesignWidth="578"
|
||||
mc:Ignorable="d"
|
||||
Width="528"
|
||||
d:DesignWidth="578"
|
||||
d:DesignHeight="350"
|
||||
x:Class="Ryujinx.Ava.UI.Views.User.UserFirmwareAvatarSelectorView"
|
||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
x:DataType="viewModels:UserFirmwareAvatarSelectorViewModel"
|
||||
Focusable="True">
|
||||
Focusable="True"
|
||||
x:DataType="viewModels:UserFirmwareAvatarSelectorViewModel">
|
||||
<Design.DataContext>
|
||||
<viewModels:UserFirmwareAvatarSelectorViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto,Auto">
|
||||
<ListBox
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto,Auto">
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
BorderThickness="0"
|
||||
SelectedIndex="{Binding SelectedIndex}"
|
||||
Height="400"
|
||||
ItemsSource="{Binding Images}"
|
||||
Padding="2.5"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
|
||||
CornerRadius="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel
|
||||
Orientation="Horizontal"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="CornerRadius" Value="4" />
|
||||
<Setter Property="Width" Value="85" />
|
||||
<Setter Property="MaxWidth" Value="85" />
|
||||
<Setter Property="MinWidth" Value="85" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem /template/ Rectangle#SelectionIndicator">
|
||||
<Setter Property="MinHeight" Value="70" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Panel
|
||||
Background="{Binding BackgroundColor}"
|
||||
Margin="5">
|
||||
<Image Source="{Binding Data, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
|
||||
</Panel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
VerticalAlignment="Stretch">
|
||||
<ListBox
|
||||
Grid.Row="1"
|
||||
Background="Transparent"
|
||||
SelectedIndex="{Binding SelectedIndex}"
|
||||
Height="400"
|
||||
ItemsSource="{Binding Images}">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel
|
||||
Orientation="Horizontal"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Center" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="CornerRadius" Value="4" />
|
||||
<Setter Property="Width" Value="85" />
|
||||
<Setter Property="MaxWidth" Value="85" />
|
||||
<Setter Property="MinWidth" Value="85" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem /template/ Rectangle#SelectionIndicator">
|
||||
<Setter Property="MinHeight" Value="70" />
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:selected /template/ Rectangle#SelectionIndicator">
|
||||
<Setter Property="IsVisible" Value="True" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Panel
|
||||
Background="{Binding BackgroundColor}"
|
||||
Margin="5">
|
||||
<Image Source="{Binding Data, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
|
||||
</Panel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
<StackPanel
|
||||
Grid.Row="3"
|
||||
Orientation="Horizontal"
|
||||
Spacing="10"
|
||||
Margin="0 24 0 0"
|
||||
Margin="0,30,0,0"
|
||||
HorizontalAlignment="Left">
|
||||
<Button
|
||||
Width="50"
|
||||
MinWidth="50"
|
||||
Height="35"
|
||||
Height="37"
|
||||
Click="GoBack">
|
||||
<ui:SymbolIcon Symbol="Back" />
|
||||
</Button>
|
||||
@@ -76,7 +84,7 @@
|
||||
Grid.Row="3"
|
||||
Orientation="Horizontal"
|
||||
Spacing="10"
|
||||
Margin="0 24 0 0"
|
||||
Margin="0,30,0,0"
|
||||
HorizontalAlignment="Right">
|
||||
<ui:ColorPickerButton
|
||||
FlyoutPlacement="Top"
|
||||
@@ -95,10 +103,10 @@
|
||||
</ui:ColorPickerButton.Styles>
|
||||
</ui:ColorPickerButton>
|
||||
<Button
|
||||
Content="{ext:Locale AvatarChoose}"
|
||||
Height="35"
|
||||
Name="ChooseButton"
|
||||
Click="ChooseButton_OnClick" />
|
||||
Height="37"
|
||||
Click="ChooseButton_OnClick">
|
||||
<TextBlock Text="{ext:Locale UserProfiles_ButtonChooseAvatar}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using FluentAvalonia.UI.Navigation;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using SkiaSharp;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.User
|
||||
{
|
||||
@@ -14,44 +17,53 @@ namespace Ryujinx.Ava.UI.Views.User
|
||||
{
|
||||
private NavigationDialogHost _parent;
|
||||
private TempProfile _profile;
|
||||
public ContentManager ContentManager { get; private set; }
|
||||
public UserFirmwareAvatarSelectorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
ViewModel = new UserFirmwareAvatarSelectorViewModel();
|
||||
DataContext = ViewModel;
|
||||
AddHandler(Frame.NavigatedToEvent, (s, e) => NavigatedTo(e), RoutingStrategies.Direct);
|
||||
}
|
||||
|
||||
public UserFirmwareAvatarSelectorView(ContentManager contentManager)
|
||||
{
|
||||
ContentManager = contentManager;
|
||||
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public UserFirmwareAvatarSelectorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
||||
{
|
||||
NavigatedTo(e);
|
||||
}, RoutingStrategies.Direct);
|
||||
ViewModel = new UserFirmwareAvatarSelectorViewModel();
|
||||
DataContext = ViewModel;
|
||||
AddHandler(Frame.NavigatedToEvent, (s, e) => NavigatedTo(e), RoutingStrategies.Direct);
|
||||
}
|
||||
|
||||
private void NavigatedTo(NavigationEventArgs arg)
|
||||
{
|
||||
if (Program.PreviewerDetached)
|
||||
if (!Program.PreviewerDetached)
|
||||
return;
|
||||
|
||||
if (arg.NavigationMode != NavigationMode.New)
|
||||
return;
|
||||
|
||||
(_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter;
|
||||
ContentManager = _parent.ContentManager;
|
||||
|
||||
((ContentDialog)_parent.Parent).Title =
|
||||
$"{LocaleManager.Instance[LocaleKeys.UserProfiles_WindowTitle]} - " +
|
||||
$"{LocaleManager.Instance[LocaleKeys.UserProfiles_SelectAvatarTitle]}";
|
||||
|
||||
ViewModel.SelectedIndex = -1;
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
if (arg.NavigationMode == NavigationMode.New)
|
||||
bool found = ContentManager.GetCurrentFirmwareVersion() != null;
|
||||
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
(_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter;
|
||||
ContentManager = _parent.ContentManager;
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
ViewModel = new UserFirmwareAvatarSelectorViewModel();
|
||||
}
|
||||
|
||||
DataContext = ViewModel;
|
||||
}
|
||||
}
|
||||
ViewModel.FirmwareFound = found;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public ContentManager ContentManager { get; private set; }
|
||||
|
||||
private void GoBack(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_parent.GoBack();
|
||||
@@ -59,32 +71,31 @@ namespace Ryujinx.Ava.UI.Views.User
|
||||
|
||||
private void ChooseButton_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ViewModel.SelectedImage != null)
|
||||
if (ViewModel.SelectedImage == null)
|
||||
return;
|
||||
|
||||
using MemoryStream streamJpg = new();
|
||||
using SKBitmap bitmap = SKBitmap.Decode(ViewModel.SelectedImage);
|
||||
using SKBitmap newBitmap = new(bitmap.Width, bitmap.Height);
|
||||
|
||||
using (SKCanvas canvas = new(newBitmap))
|
||||
{
|
||||
using MemoryStream streamJpg = new();
|
||||
using SKBitmap bitmap = SKBitmap.Decode(ViewModel.SelectedImage);
|
||||
using SKBitmap newBitmap = new(bitmap.Width, bitmap.Height);
|
||||
|
||||
using (SKCanvas canvas = new(newBitmap))
|
||||
{
|
||||
canvas.Clear(new SKColor(
|
||||
ViewModel.BackgroundColor.R,
|
||||
ViewModel.BackgroundColor.G,
|
||||
ViewModel.BackgroundColor.B,
|
||||
ViewModel.BackgroundColor.A));
|
||||
canvas.DrawBitmap(bitmap, 0, 0);
|
||||
}
|
||||
|
||||
using (SKImage image = SKImage.FromBitmap(newBitmap))
|
||||
using (SKData dataJpeg = image.Encode(SKEncodedImageFormat.Jpeg, 100))
|
||||
{
|
||||
dataJpeg.SaveTo(streamJpg);
|
||||
}
|
||||
|
||||
_profile.Image = streamJpg.ToArray();
|
||||
|
||||
_parent.GoBack();
|
||||
canvas.Clear(new SKColor(
|
||||
ViewModel.BackgroundColor.R,
|
||||
ViewModel.BackgroundColor.G,
|
||||
ViewModel.BackgroundColor.B,
|
||||
ViewModel.BackgroundColor.A));
|
||||
canvas.DrawBitmap(bitmap, 0, 0);
|
||||
}
|
||||
|
||||
using (SKImage image = SKImage.FromBitmap(newBitmap))
|
||||
using (SKData dataJpeg = image.Encode(SKEncodedImageFormat.Jpeg, 100))
|
||||
{
|
||||
dataJpeg.SaveTo(streamJpg);
|
||||
}
|
||||
|
||||
_profile.Image = streamJpg.ToArray();
|
||||
_parent.GoBack();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
<UserControl
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:viewModles="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
Focusable="True"
|
||||
mc:Ignorable="d"
|
||||
x:Class="Ryujinx.Ava.UI.Views.User.UserProfileImageSelectorView"
|
||||
x:DataType="viewModles:UserProfileImageSelectorViewModel"
|
||||
Width="500"
|
||||
d:DesignWidth="500">
|
||||
<Design.DataContext>
|
||||
<viewModles:UserProfileImageSelectorViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center" RowDefinitions="Auto,70,Auto">
|
||||
<TextBlock
|
||||
Grid.Row="0"
|
||||
TextWrapping="Wrap"
|
||||
HorizontalAlignment="Left"
|
||||
TextAlignment="Start"
|
||||
Text="{ext:Locale ProfileImageSelectionNote}" />
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Spacing="10"
|
||||
HorizontalAlignment="Left"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Width="50"
|
||||
MinWidth="50"
|
||||
Click="GoBack">
|
||||
<ui:SymbolIcon Symbol="Back" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Spacing="10"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Name="Import"
|
||||
Click="Import_OnClick">
|
||||
<TextBlock Text="{ext:Locale ProfileImageSelectionImportImage}" />
|
||||
</Button>
|
||||
<Button
|
||||
Name="SelectFirmwareImage"
|
||||
IsEnabled="{Binding FirmwareFound}"
|
||||
Click="SelectFirmwareImage_OnClick">
|
||||
<TextBlock Text="{ext:Locale ProfileImageSelectionSelectAvatar}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,118 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.VisualTree;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using FluentAvalonia.UI.Navigation;
|
||||
using Ryujinx.Ava.Common.Locale;
|
||||
using Ryujinx.Ava.UI.Controls;
|
||||
using Ryujinx.Ava.UI.Models;
|
||||
using Ryujinx.Ava.UI.ViewModels;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using SkiaSharp;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Ryujinx.Ava.UI.Views.User
|
||||
{
|
||||
public partial class UserProfileImageSelectorView : RyujinxControl<UserProfileImageSelectorViewModel>
|
||||
{
|
||||
private ContentManager _contentManager;
|
||||
private NavigationDialogHost _parent;
|
||||
private TempProfile _profile;
|
||||
|
||||
public UserProfileImageSelectorView()
|
||||
{
|
||||
InitializeComponent();
|
||||
AddHandler(Frame.NavigatedToEvent, (s, e) =>
|
||||
{
|
||||
NavigatedTo(e);
|
||||
}, RoutingStrategies.Direct);
|
||||
}
|
||||
|
||||
private void NavigatedTo(NavigationEventArgs arg)
|
||||
{
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
switch (arg.NavigationMode)
|
||||
{
|
||||
case NavigationMode.New:
|
||||
(_parent, _profile) = ((NavigationDialogHost, TempProfile))arg.Parameter;
|
||||
_contentManager = _parent.ContentManager;
|
||||
|
||||
((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - {LocaleManager.Instance[LocaleKeys.ProfileImageSelectionHeader]}";
|
||||
|
||||
if (Program.PreviewerDetached)
|
||||
{
|
||||
DataContext = ViewModel = new UserProfileImageSelectorViewModel();
|
||||
ViewModel.FirmwareFound = _contentManager.GetCurrentFirmwareVersion() != null;
|
||||
}
|
||||
|
||||
break;
|
||||
case NavigationMode.Back:
|
||||
if (_profile.Image != null)
|
||||
{
|
||||
_parent.GoBack();
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void Import_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
IReadOnlyList<IStorageFile> result = await ((Window)this.GetVisualRoot()!).StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter = new List<FilePickerFileType>
|
||||
{
|
||||
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
|
||||
{
|
||||
Patterns = ["*.jpg", "*.jpeg", "*.png", "*.bmp"],
|
||||
AppleUniformTypeIdentifiers = ["public.jpeg", "public.png", "com.microsoft.bmp"],
|
||||
MimeTypes = ["image/jpeg", "image/png", "image/bmp"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (result.Count > 0)
|
||||
{
|
||||
_profile.Image = ProcessProfileImage(File.ReadAllBytes(result[0].Path.LocalPath));
|
||||
_parent.GoBack();
|
||||
}
|
||||
}
|
||||
|
||||
private void GoBack(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_parent.GoBack();
|
||||
}
|
||||
|
||||
private void SelectFirmwareImage_OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ViewModel.FirmwareFound)
|
||||
{
|
||||
_parent.Navigate(typeof(UserFirmwareAvatarSelectorView), (_parent, _profile));
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] ProcessProfileImage(byte[] buffer)
|
||||
{
|
||||
using SKBitmap bitmap = SKBitmap.Decode(buffer);
|
||||
|
||||
SKBitmap resizedBitmap = bitmap.Resize(new SKImageInfo(256, 256), SKFilterQuality.High);
|
||||
|
||||
using MemoryStream streamJpg = new();
|
||||
|
||||
if (resizedBitmap != null)
|
||||
{
|
||||
using SKImage image = SKImage.FromBitmap(resizedBitmap);
|
||||
using SKData dataJpeg = image.Encode(SKEncodedImageFormat.Jpeg, 100);
|
||||
|
||||
dataJpeg.SaveTo(streamJpg);
|
||||
}
|
||||
|
||||
return streamJpg.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +1,64 @@
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Views.User.UserRecovererView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
d:DesignWidth="550"
|
||||
d:DesignHeight="450"
|
||||
mc:Ignorable="d"
|
||||
Width="500"
|
||||
Height="400"
|
||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
x:Class="Ryujinx.Ava.UI.Views.User.UserRecovererView"
|
||||
x:DataType="viewModels:UserProfileViewModel"
|
||||
Focusable="True">
|
||||
Focusable="True"
|
||||
x:DataType="viewModels:UserProfileViewModel">
|
||||
<Design.DataContext>
|
||||
<viewModels:UserProfileViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch" RowDefinitions="*,Auto">
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RowDefinitions="*,Auto">
|
||||
<Border
|
||||
CornerRadius="5"
|
||||
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
|
||||
BorderThickness="1"
|
||||
Grid.Row="0">
|
||||
Grid.Row="0"
|
||||
Padding="2.5">
|
||||
<Panel>
|
||||
<ListBox
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ItemsSource="{Binding LostProfiles}">
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Padding" Value="10" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:selected /template/ Rectangle#SelectionIndicator">
|
||||
<Setter Property="IsVisible" Value="False" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border
|
||||
Margin="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ClipToBounds="True"
|
||||
CornerRadius="5">
|
||||
CornerRadius="4">
|
||||
<Grid Margin="0" ColumnDefinitions="*,Auto">
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="5"
|
||||
Text="{Binding UserId}"
|
||||
TextAlignment="Start"
|
||||
TextWrapping="Wrap" />
|
||||
<Button Grid.Column="1"
|
||||
HorizontalAlignment="Right"
|
||||
Click="Recover"
|
||||
CommandParameter="{Binding}"
|
||||
Content="{ext:Locale Recover}"/>
|
||||
Margin="5"
|
||||
Command="{Binding Recover}"
|
||||
CommandParameter="{Binding}">
|
||||
<TextBlock Text="{ext:Locale UserProfiles_RecoverProfile}" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
@@ -56,12 +67,12 @@
|
||||
<TextBlock
|
||||
IsVisible="{Binding IsEmpty}"
|
||||
TextAlignment="Center"
|
||||
Text="{ext:Locale UserProfilesRecoverEmptyList}"/>
|
||||
Text="{ext:Locale UserProfiles_RecoverProfile_EmptyList}"/>
|
||||
</Panel>
|
||||
</Border>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="0 24 0 0"
|
||||
Margin="0,30,0,0"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Width="50"
|
||||
|
||||
@@ -27,11 +27,12 @@ namespace Ryujinx.Ava.UI.Views.User
|
||||
switch (arg.NavigationMode)
|
||||
{
|
||||
case NavigationMode.New:
|
||||
case NavigationMode.Back:
|
||||
NavigationDialogHost parent = (NavigationDialogHost)arg.Parameter;
|
||||
|
||||
_parent = parent;
|
||||
|
||||
((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - {LocaleManager.Instance[LocaleKeys.UserProfilesRecoverHeading]}";
|
||||
((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfiles_WindowTitle]} - {LocaleManager.Instance[LocaleKeys.UserProfiles_RecoverLostProfiles]}";
|
||||
|
||||
break;
|
||||
}
|
||||
@@ -42,10 +43,5 @@ namespace Ryujinx.Ava.UI.Views.User
|
||||
{
|
||||
_parent?.GoBack();
|
||||
}
|
||||
|
||||
private void Recover(object sender, RoutedEventArgs e)
|
||||
{
|
||||
_parent?.RecoverLostAccounts();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<UserControl
|
||||
x:Class="Ryujinx.Ava.UI.Views.User.UserSaveManagerView"
|
||||
xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
@@ -13,7 +14,6 @@
|
||||
d:DesignHeight="500"
|
||||
Height="450"
|
||||
Width="550"
|
||||
x:Class="Ryujinx.Ava.UI.Views.User.UserSaveManagerView"
|
||||
x:DataType="viewModels:UserSaveManagerViewModel"
|
||||
Focusable="True">
|
||||
<Design.DataContext>
|
||||
@@ -22,9 +22,9 @@
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
Margin="0,0,0,5"
|
||||
HorizontalAlignment="Stretch" ColumnDefinitions="Auto,*">
|
||||
<StackPanel
|
||||
Margin="0,0,0,10"
|
||||
Spacing="10"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Left"
|
||||
@@ -33,9 +33,9 @@
|
||||
HorizontalContentAlignment="Left"
|
||||
MinWidth="100">
|
||||
<ComboBoxItem
|
||||
Content="{ext:Locale Name}" />
|
||||
Content="{ext:Locale UserProfiles_ManageSaves_SortByName}" />
|
||||
<ComboBoxItem
|
||||
Content="{ext:Locale Size}" />
|
||||
Content="{ext:Locale UserProfiles_ManageSaves_SortBySize}" />
|
||||
<ComboBox.Styles>
|
||||
<Style Selector="ContentControl#ContentPresenter">
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
@@ -46,9 +46,9 @@
|
||||
HorizontalContentAlignment="Left"
|
||||
MinWidth="150">
|
||||
<ComboBoxItem
|
||||
Content="{ext:Locale OrderAscending}" />
|
||||
Content="{ext:Locale UserProfiles_ManageSaves_SortOrderAscending}" />
|
||||
<ComboBoxItem
|
||||
Content="{ext:Locale OrderDescending}" />
|
||||
Content="{ext:Locale UserProfiles_ManageSaves_SortOrderDescending}" />
|
||||
<ComboBox.Styles>
|
||||
<Style Selector="ContentControl#ContentPresenter">
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
@@ -59,18 +59,18 @@
|
||||
<Grid
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Margin="10,0, 0, 0" ColumnDefinitions="Auto,*">
|
||||
<TextBlock Text="{ext:Locale Search}" VerticalAlignment="Center" />
|
||||
Margin="20,0,0,10" ColumnDefinitions="Auto,*">
|
||||
<TextBox
|
||||
Margin="10,0,0,0"
|
||||
Margin="5,0,0,0"
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Text="{Binding Search}" />
|
||||
Text="{Binding Search}"
|
||||
Watermark="{ext:Locale UserProfiles_ManageSaves_Search}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Border
|
||||
Grid.Row="1"
|
||||
Margin="0,5"
|
||||
Padding="2.5"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
|
||||
CornerRadius="5"
|
||||
@@ -84,7 +84,7 @@
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Padding" Value="10" />
|
||||
<Setter Property="Margin" Value="5" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="4" />
|
||||
</Style>
|
||||
<Style Selector="ListBoxItem:selected /template/ Rectangle#SelectionIndicator">
|
||||
@@ -168,7 +168,7 @@
|
||||
</Border>
|
||||
<StackPanel
|
||||
Grid.Row="2"
|
||||
Margin="0 24 0 0"
|
||||
Margin="0,30,0,0"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Width="50"
|
||||
|
||||
@@ -55,7 +55,7 @@ namespace Ryujinx.Ava.UI.Views.User
|
||||
}
|
||||
|
||||
DataContext = ViewModel = new UserSaveManagerViewModel(_accountManager);
|
||||
((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle]} - {ViewModel.SaveManagerHeading}";
|
||||
((ContentDialog)_parent.Parent).Title = $"{LocaleManager.Instance[LocaleKeys.UserProfiles_WindowTitle]} - {ViewModel.SaveManagerTitle}";
|
||||
|
||||
Task.Run(LoadSaves);
|
||||
}
|
||||
@@ -127,8 +127,8 @@ namespace Ryujinx.Ava.UI.Views.User
|
||||
{
|
||||
if (button.DataContext is SaveModel saveModel)
|
||||
{
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.DeleteUserSave],
|
||||
LocaleManager.Instance[LocaleKeys.IrreversibleActionNote],
|
||||
UserResult result = await ContentDialogHelper.CreateConfirmationDialog(LocaleManager.Instance[LocaleKeys.UserProfiles_DeleteSaveNote],
|
||||
LocaleManager.Instance[LocaleKeys.UserProfiles_IrreversibleActionNote],
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogYes],
|
||||
LocaleManager.Instance[LocaleKeys.InputDialogNo],
|
||||
string.Empty);
|
||||
|
||||
@@ -4,43 +4,39 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:helpers="clr-namespace:Ryujinx.Ava.UI.Helpers"
|
||||
xmlns:models="clr-namespace:Ryujinx.Ava.UI.Models"
|
||||
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
|
||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
d:DesignHeight="450"
|
||||
MinWidth="500"
|
||||
d:DesignWidth="800"
|
||||
mc:Ignorable="d"
|
||||
MinWidth="500"
|
||||
Focusable="True"
|
||||
x:DataType="viewModels:UserProfileViewModel">
|
||||
<Design.DataContext>
|
||||
<viewModels:UserProfileViewModel />
|
||||
</Design.DataContext>
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RowDefinitions="*,Auto">
|
||||
<Border
|
||||
CornerRadius="5"
|
||||
BorderBrush="{DynamicResource AppListHoverBackgroundColor}"
|
||||
BorderThickness="1">
|
||||
<Border Padding="-3" BorderThickness="0">
|
||||
<ListBox
|
||||
MaxHeight="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
SelectionChanged="ProfilesList_SelectionChanged"
|
||||
Background="Transparent"
|
||||
ItemsSource="{Binding Profiles}">
|
||||
ItemsSource="{Binding Profiles}"
|
||||
SelectionChanged="ProfilesList_SelectionChanged">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel
|
||||
HorizontalAlignment="Left"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Orientation="Horizontal"/>
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Margin" Value="5 5 0 5" />
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="5" />
|
||||
</Style>
|
||||
<Style Selector="Rectangle#SelectionIndicator">
|
||||
@@ -54,6 +50,7 @@
|
||||
PointerEntered="Grid_PointerEntered"
|
||||
PointerExited="Grid_OnPointerExited">
|
||||
<Border
|
||||
Margin="5"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
ClipToBounds="True"
|
||||
@@ -65,18 +62,20 @@
|
||||
<Image
|
||||
Width="96"
|
||||
Height="96"
|
||||
Margin="0,0,0,10"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
Source="{Binding Image, Converter={x:Static helpers:BitmapArrayValueConverter.Instance}}" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Stretch"
|
||||
MaxWidth="90"
|
||||
Text="{Binding Name}"
|
||||
Height="30"
|
||||
MaxWidth="90"
|
||||
TextAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="2"
|
||||
Margin="5" />
|
||||
TextWrapping="Wrap"
|
||||
MaxLines="2" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<Border
|
||||
@@ -89,10 +88,10 @@
|
||||
Background="{DynamicResource ThemeContentBackgroundColor}"
|
||||
IsVisible="{Binding IsPointerOver}">
|
||||
<Button
|
||||
MaxHeight="24"
|
||||
MaxWidth="24"
|
||||
MinHeight="24"
|
||||
MinWidth="24"
|
||||
MaxHeight="24"
|
||||
MaxWidth="24"
|
||||
CornerRadius="12"
|
||||
Padding="0"
|
||||
Click="EditUser">
|
||||
@@ -104,8 +103,8 @@
|
||||
<DataTemplate
|
||||
DataType="viewModels:BaseModel">
|
||||
<Panel
|
||||
Height="118"
|
||||
Width="96">
|
||||
Height="146"
|
||||
Width="106">
|
||||
<Button
|
||||
MinWidth="50"
|
||||
MinHeight="50"
|
||||
@@ -119,11 +118,6 @@
|
||||
Click="AddUser">
|
||||
<ui:SymbolIcon Symbol="Add" />
|
||||
</Button>
|
||||
<Panel.Styles>
|
||||
<Style Selector="Panel">
|
||||
<Setter Property="Background" Value="{DynamicResource ListBoxBackground}"/>
|
||||
</Style>
|
||||
</Panel.Styles>
|
||||
</Panel>
|
||||
</DataTemplate>
|
||||
</ListBox.DataTemplates>
|
||||
@@ -131,27 +125,29 @@
|
||||
</Border>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="0 24 0 0"
|
||||
Margin="0,30,0,0"
|
||||
HorizontalAlignment="Left"
|
||||
Orientation="Horizontal"
|
||||
Spacing="10">
|
||||
<Button
|
||||
Click="ManageSaves">
|
||||
<TextBlock Text="{ext:Locale UserProfilesManageSaves}" />
|
||||
<TextBlock Text="{ext:Locale UserProfiles_ManageSaves}" />
|
||||
</Button>
|
||||
<Button
|
||||
Click="RecoverLostAccounts">
|
||||
<TextBlock Text="{ext:Locale UserProfilesRecoverLostAccounts}" />
|
||||
<TextBlock
|
||||
Text="{ext:Locale UserProfiles_RecoverLostProfiles}"
|
||||
ToolTip.Tip="{ext:Locale UserProfiles_RecoverLostProfiles_ToolTip}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="0 24 0 0"
|
||||
Margin="0,30,0,0"
|
||||
HorizontalAlignment="Right"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
Click="Close">
|
||||
<TextBlock Text="{ext:Locale UserProfilesClose}" />
|
||||
<TextBlock Text="{ext:Locale UserProfiles_ButtonClose}" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace Ryujinx.Ava.UI.Views.User
|
||||
|
||||
if (arg.NavigationMode == NavigationMode.Back)
|
||||
{
|
||||
((ContentDialog)_parent.Parent).Title = LocaleManager.Instance[LocaleKeys.UserProfileWindowTitle];
|
||||
((ContentDialog)_parent.Parent).Title = LocaleManager.Instance[LocaleKeys.UserProfiles_WindowTitle];
|
||||
}
|
||||
|
||||
DataContext = ViewModel;
|
||||
|
||||
@@ -41,6 +41,8 @@
|
||||
<KeyBinding Gesture="Escape" Command="{Binding ExitCurrentState}" />
|
||||
<KeyBinding Gesture="Ctrl+A" Command="{Binding OpenAmiiboWindow}" />
|
||||
<KeyBinding Gesture="Ctrl+B" Command="{Binding OpenBinFile}" />
|
||||
<KeyBinding Gesture="Ctrl+Shift+R" Command="{Binding ReloadRenderDocApi}" />
|
||||
<KeyBinding Gesture="Ctrl+Shift+C" Command="{Binding ToggleCapture}" />
|
||||
</Window.KeyBindings>
|
||||
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RowDefinitions="*">
|
||||
<helpers:OffscreenTextBox IsEnabled="False" Opacity="0" Name="HiddenTextBox" IsHitTestVisible="False" IsTabStop="False" />
|
||||
|
||||
@@ -19,6 +19,9 @@ namespace Ryujinx.Ava.Utilities
|
||||
public static string OverrideSystemLanguage { get; private set; }
|
||||
public static string OverrideHideCursor { get; private set; }
|
||||
public static string BaseDirPathArg { get; private set; }
|
||||
|
||||
public static string RenderDocCaptureTitleFormat { get; private set; } =
|
||||
"{EmuVersion}\n{GuestName} {GuestVersion} {GuestTitleId} {GuestArch}";
|
||||
public static Optional<FilePath> FirmwareToInstallPathArg { get; set; }
|
||||
public static string Profile { get; private set; }
|
||||
public static string LaunchPathArg { get; private set; }
|
||||
@@ -54,6 +57,20 @@ namespace Ryujinx.Ava.Utilities
|
||||
|
||||
BaseDirPathArg = args[++i];
|
||||
|
||||
arguments.Add(arg);
|
||||
arguments.Add(args[i]);
|
||||
break;
|
||||
case "-rdct":
|
||||
case "--rd-capture-title-format":
|
||||
if (i + 1 >= args.Length)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
RenderDocCaptureTitleFormat = args[++i];
|
||||
|
||||
arguments.Add(arg);
|
||||
arguments.Add(args[i]);
|
||||
break;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Gommon;
|
||||
using Ryujinx.HLE.Loaders.Processes;
|
||||
|
||||
namespace Ryujinx.Ava.Utilities
|
||||
@@ -22,5 +23,23 @@ namespace Ryujinx.Ava.Utilities
|
||||
? appTitle + $" ({pauseString})"
|
||||
: appTitle;
|
||||
}
|
||||
|
||||
public static string FormatRenderDocCaptureTitle(ProcessResult activeProcess, string applicationVersion)
|
||||
{
|
||||
if (activeProcess == null)
|
||||
return string.Empty;
|
||||
|
||||
string titleNameSection = string.IsNullOrWhiteSpace(activeProcess.Name) ? string.Empty : activeProcess.Name;
|
||||
string titleVersionSection = string.IsNullOrWhiteSpace(activeProcess.DisplayVersion) ? string.Empty : $"v{activeProcess.DisplayVersion}";
|
||||
string titleIdSection = $"({activeProcess.ProgramIdText.ToUpper()})";
|
||||
string titleArchSection = activeProcess.Is64Bit ? "(64-bit)" : "(32-bit)";
|
||||
|
||||
return CommandLineState.RenderDocCaptureTitleFormat
|
||||
.ReplaceIgnoreCase("{EmuVersion}", applicationVersion)
|
||||
.ReplaceIgnoreCase("{GuestName}", titleNameSection)
|
||||
.ReplaceIgnoreCase("{GuestVersion}", titleVersionSection)
|
||||
.ReplaceIgnoreCase("{GuestTitleId}", titleIdSection)
|
||||
.ReplaceIgnoreCase("{GuestArch}", titleArchSection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user