Compare commits

...

13 Commits

Author SHA1 Message Date
LotP b19e2ac65d mono-jit (#167)
- fix alignment issue with jitcache that could cause a crash.
- Reimplement sparse-jit to only use 1 layer.
    - the old 2 layer approach basically only ever used 1 path due to the bottom-layer size, so doing a double look-up seemed wasteful.
    - changed memory block allocation stategy to allocate based on requested code range instead of the possible address-space.

Co-authored-by: LotP1 <68976644+LotP1@users.noreply.github.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/167
2026-07-10 11:16:55 +00:00
der_zelda_player 6953128ff2 Added a lot of German translations (#166)
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/166
2026-07-06 21:56:53 +00:00
_Neo_ b72990bf53 UI: Actions Menu (Part 3 of 4) → Amiibo & Skylanders (#158)
Ayyyy, welcome to the UI: Actions Menu → Amiibo & Skylanders PR!

WOOOOO!!!!

This is the third PR in a series aimed at delivering the largest overhaul and improvements to the `Actions` menu yet.

This PR introduces visual and functionality improvements to the two `Scan Amiibo` options, as well as improving `Scan Skylanders`.

### GENERAL
* **Renamed:** Open Mii Applet → Open Mii Editor in `MainMenuBarView.axaml.cs`

### LOCALISATION
* **Fractured:** More locales
    * `Amiibo.json` - all UI-related locales to the `Scan Amiibo` window
    * `Common_Buttons.json` - all locales related to buttons
    * `Common_FilePicker.json` - all locales related to the file picker that are implemented in many places
    * `Dialog_Amiibo.json` - all Amiibo dialogs
    * `Dialog_Skylanders.json` - all Skylander dialogs
* **Removed:** Some weird duplicated locales (they appeared after some sort of rebase, but they were few)

**NOTE:** `Common_Buttons.json` and `Common_FilePicker.json` were not fully populated, but many more locales will be added and cleaned up in future PRs.

### SCAN AMIIBO (WINDOW)
* **Improved:** The UI
    * The UI now more cleanly utilises both Normal and Flush Controls.
    * All buttons have been moved to the top row and features a whole lot more updates (read below).
* **Reworded:** `Hack: Random UUID Tag` → `Use Random UUID`
    * The option is inherently already a hack/workaround, therefore, having "Hack" is not necessary. It is also implied that it's a tag.
* **Implemented:** [Feature Request #271](https://github.com/Ryubing/Issues/issues/271)
    * The feature request is for the ability to pause emulation while navigating the `Scan Amiibo` window (so as to reduce/remove timeouts) and for an Amiibo "name" sorting feature. You asked, we deliver!
    * **Pause Emulation:**
        * The top row now features a "setting" button dropdown, which houses `Use Random UUID` and `Pause Emulation While Scanning`
            * Note: Both options were moved to a button dropdown so as to not have to increase window size by a 1000x
        * `Pause Emulation While Scanning` is a dynamic, real-time implementation.
            * By default, the checkbox is not checked.
            * When it's checked, emulation _instantly_ pauses for the duration while the window is open. Pressing "Scan Amiibo" or "Cancel" resumes emulation. Reopening the window pauses the emulation instantly as well.
            * This setting persists both while in-game and upon restarting Ryujinx, so users don't have to constantly go back and check the option again.
            * Additionally, this feature has been propagated to `Scan Amiibo (BIN)`. Perhaps some users would also want emulation to pause while scanning via BIN, so if the checkbox is checked, it'll pause emulation when the file picker is open as well.
    * **Sorting & Search:**
        * Additionally, implemented a simple "sort by name", in ascending and descending order.
        * Added a search bar (related to the change below) to help with searching through the long Amiibo list.
        * Search is semi-alphabetical, semi-series based. If I want to search for Samus Amiibo, then if I type "Samus", it'll first show Dark Samus, as it appears in a series earlier in the list. This behaviour can be modified to just be alphabetical, but it is also more categorisation.
* **Modified & Renamed:** `Show Incompatible Amiibo` → `Show All Amiibo`
    * Pressing `Show Incompatible Amiibo` would display all Amiibo available, so the option was renamed.
    * The option now forces all Amiibo to appear in a list, without a preselected series. Unchecked the option restores original behaviour by breaking down Amiibo by their series (and only those directly compatible with the game).
* **Removed:** Series label
    * It's clear that it's an Amiibo series.
* **Fixed:** Amiibo Usage visual structure
    * When an Amiibo had multiple in-game usages (multiple purposes), they would all be squished together with no spaces in between them, reducing readability.
    * Now, they are all separated and given an extra line between each usage.
    * Additionally, fixed "Unknown" text position as well by giving it another extra empty line.
* **Renamed:** Writeable to Usage (Can Save Data)
    * Usage and Usage (Can Save Data) are separate strings because some languages must add space before or after the colon
* **Fixed:** Sometimes incorrect Amiibo images could be shown when pressing `Show Incompatible Amiibo`/`Show All Amiibo` in certain instances.
    * Sometimes = pressing the checkbox 10 or 20...maybe 30 times in a row (I pressed it a lot!)
* **Reworded:** Cabinet Dialog strings
    * Cabinet Dialog is the dialog that appears when you want to rename your Amiibo. Previously, the options didn't reflect the actual window and what they were asking the user, but now clearly tell the user that they are renaming their Amiibo, and when the dialog prompts the user to scan their amiibo again.

### SCAN AMIIBO (BIN)
* **Renamed:** `Scan Amiibo (.BIN)` to `Scan Amiibo (BIN)` to be similar to `Install Keys` > `KEYS` and similar menu items
* **Adjusted:** File picker title when loading Amiibo from BIN to...literally tell the user what to select and what will happen (similar to `Install Firmware` and `Install Keys` file picker titles)

### SCAN/REMOVE SKYLANDER
* **Merged:** `Scan Skylander` and `Remove Skylander`
    * Instead of having two separate buttons, let's have one button that actively updates state. Instead of using two shortcuts, it now uses one, and the label updates based on whether the game is asking to `Scan Skylander` or whether you can `Remove Skylander`.
* **Fixed:** `Scan Skylander`/`Remove Skylander` were not visible properly
    * Initial implementation only displayed the menu options/buttons when a particular Skylander Imaginators game was being played (i.e. specific ID)
    * While there is only one Skylander game on the Switch, the game has been distributed in multiple regions under different game IDs and has different languages. People who owned Skylander Imaginators with a 0100ccc0002e6000 ID could use this feature and _nobody else._
    * This has now been fixed by allowing those options to be visible at all times, regardless of the game
        * For Skylander games, this is not an issue because the backend implementation for scanning/removing a skylander is the same (tested)
        * For other games, then it doesn't matter since the feature is not used, but it will let users know the feature exists, in case they wished to previously play the game, but couldn't due to the missing backend.
* **Renamed:** Skylander functions and methods
    * You don't open a window (like Scan Amiibo) but you open a file picker.
* **Fixed:** Inconsistent button state and shortcut update
    * This was a bug in the previous implementation as well - you couldn't scan/remove skylander with the keyboard shortcut unless you opened the `Actions` menu (and only then would the shortcut register). This has been fixed.

As such...Skylanders Imaginators are now **Playable**! Updated the entry and added another game ID.

_If there are any features or changes that you wish to be implemented, please comment down below and I'll be happy to accommodate!_

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/158
2026-07-06 14:36:00 +00:00
Max 732e882524 Included more Discord title IDs (#165)
- Skylanders: Imaginators (US + EU)
- Dragon Quest VII Reimagined Demo
- Rhythm Heaven Groove: Starter Demo
- MARVEL Cosmic Invasion Demo

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/165
2026-07-06 14:29:05 +00:00
MrKev 68432d210b VP9-improvements (#132)
- Improves MMNV request handling by tracking active clock requests per session and returning the effective requested rate for each module.
- Treats sentinel clock requests (`0` / negative values) as request clears.
- Preserves VP9 segment maps across inter frames, resetting them for key/intra/size-change cases.
- Avoids stale VP9 motion-vector state by disabling previous MV lookup on key frames and clearing output MVs for intra-only frames.

This fixes VP9 playback corruption seen in several Just Dance videos where stale segmentation/MV state could leak across frames and produce blocky/discolored output until a later refresh frame.
The MMNV changes also make the service behavior closer to what games expect when requesting and clearing NvDec clocks, removing the stub spam for this service.

Co-authored-by: MrKev312 <34964788+MrKev312@users.noreply.github.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/132
Reviewed-by: sh0inx <sh0inx@noreply.git.ryujinx.app>
2026-07-06 14:23:35 +00:00
Max e47417a161 fixed rumble not being sent to the controller (#163)
woopsies

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/163
2026-07-05 21:28:59 +00:00
Max d665de1f4f Include more games in Discord RPC (#157)
logos already included in dash

- Mary Skelter Finale
- DELTARUNE

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/157
2026-07-05 20:33:38 +00:00
awesomeangotti 680b5b719a Discord Rich Presence: Allow selective writing to multiple (two) lines of RPC from play reports (#154)
This PR implements a method inside the DiscordIntegrationModule class to allow a playreport parser (IDK if that is the actual name) to return information for both lines usable by playreports. It has complete backwards compatibility with all previous methods.

Through the magic of JSON, multiline RPC is mine to command

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/154
2026-07-05 20:29:39 +00:00
LotP b236732713 cleanup (#161)
general cleanup for some warnings in the project and some developer annoyances.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/161
2026-07-04 15:30:55 +00:00
awesomeangotti 97aed15d19 UI: Five minute coding adventure gone wrong (Fixing splashes) (#160)
With help from LotP I added error logging and made some minor adjustments to the display of splash text in cases where there was no splash to display. Thanks for dealing with my shenanigans LotP.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/160
2026-07-03 22:42:38 +00:00
der_zelda_player 134773e95f New German translations (#159)
Added new German translations.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/159
2026-07-03 22:41:46 +00:00
Martin Bai 5072904162 Fixed boot loop of Rhythm Heaven Groove (#156)
Fixed return value of GetSharedFontInOrderOfPriority to avoid being constantly rejected by the game, causing the boot loop.

Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/156
Reviewed-by: sh0inx <sh0inx@noreply.git.ryujinx.app>
2026-07-02 20:27:02 +00:00
LotP 6fb71b08ae instanced jit cache (#152)
- jit caches are no longer static.
- JitUnwindWindows now supports multiple jit caches.
- Jit caches should no longer cause crashes due to entry offset collisions.
- cpu tests now run concurrently.

Co-authored-by: LotP1 <68976644+LotP1@users.noreply.github.com>
Reviewed-on: https://git.ryujinx.app/projects/Ryubing/pulls/152
2026-06-30 13:40:18 +00:00
93 changed files with 2069 additions and 1683 deletions
+229
View File
@@ -0,0 +1,229 @@
{
"Locales": [
{
"ID": "WindowTitle",
"Translations": {
"ar_SA": "أميبو",
"de_DE": "",
"el_GR": "",
"en_US": "Amiibo",
"es_ES": null,
"fr_FR": null,
"he_IL": "אמיבו",
"it_IT": "",
"ja_JP": "",
"ko_KR": null,
"no_NO": "",
"pl_PL": "",
"pt_BR": null,
"ru_RU": null,
"sv_SE": null,
"th_TH": "อมิโบ้",
"tr_TR": "",
"uk_UA": "",
"zh_CN": null,
"zh_TW": null
}
},
{
"ID": "SeriesLabel",
"Translations": {
"ar_SA": "سلسلة",
"de_DE": "Serie",
"el_GR": "Σειρά",
"en_US": "Series",
"es_ES": "Serie",
"fr_FR": "Séries",
"he_IL": "סדרה",
"it_IT": "Serie",
"ja_JP": "シリーズ",
"ko_KR": "시리즈",
"no_NO": "Serie",
"pl_PL": "Seria",
"pt_BR": "Série",
"ru_RU": "Серия",
"sv_SE": "Serie",
"th_TH": "ซีรีส์",
"tr_TR": "Seri",
"uk_UA": "Серія",
"zh_CN": "系列",
"zh_TW": "系列"
}
},
{
"ID": "ShowAllAmiiboLabel",
"Translations": {
"ar_SA": "إظهار الكل Amiibo",
"de_DE": "Alle Amiibo anzeigen",
"el_GR": "Εμφάνιση Όλων των Amiibo",
"en_US": "Show All Amiibo",
"es_ES": "Mostrar Todas las Amiibo",
"fr_FR": "Afficher Tous les Amiibo",
"he_IL": "הצג הכל Amiibo",
"it_IT": "Mostra tutti gli Amiibo",
"ja_JP": "すべてのAmiiboを表示",
"ko_KR": "모든 Amiibo 표시",
"no_NO": "Vis alle Amiibo",
"pl_PL": "Pokaż Wszystkie Amiibo",
"pt_BR": "Mostrar Todos os Amiibo",
"ru_RU": "Показывать все Amiibo",
"sv_SE": "Visa alla Amiibo",
"th_TH": "แสดง Amiibo ทั้งหมด",
"tr_TR": "Tüm Amiibo'ları Göster",
"uk_UA": "Показати всі Amiibo",
"zh_CN": "显示所有 Amiibo",
"zh_TW": "顯示所有 Amiibo"
}
},
{
"ID": "UseRandomUuidLabel",
"Translations": {
"ar_SA": "استخدام UUID عشوائي",
"de_DE": "Zufällige UUID verwenden",
"el_GR": "Χρήση τυχαίου UUID",
"en_US": "Use Random UUID",
"es_ES": "Usar UUID aleatorio",
"fr_FR": "Utiliser un UUID aléatoire",
"he_IL": "השתמש ב-UUID אקראי",
"it_IT": "Usa UUID casuale",
"ja_JP": "ランダム UUID を使用",
"ko_KR": "무작위 UUID 사용",
"no_NO": "Bruk tilfeldig UUID",
"pl_PL": "Użyj losowego UUID",
"pt_BR": "Usar UUID aleatório",
"ru_RU": "Использовать случайный UUID",
"sv_SE": "Använd slumpmässig UUID",
"th_TH": "ใช้ UUID แบบสุ่ม",
"tr_TR": "Rastgele UUID kullan",
"uk_UA": "Використовувати випадковий UUID",
"zh_CN": "使用随机 UUID",
"zh_TW": "使用隨機 UUID"
}
},
{
"ID": "PauseEmulationWhileScanningAmiibo",
"Translations": {
"ar_SA": "",
"de_DE": "Emulation während des Scannens pausieren",
"el_GR": "",
"en_US": "Pause Emulation While Scanning",
"es_ES": "",
"fr_FR": "",
"he_IL": "",
"it_IT": "",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "",
"ru_RU": "Приостанавливать эмуляцию во время сканирования",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "UsageLabel",
"Translations": {
"ar_SA": "الاستخدام:",
"de_DE": "Nutzung:",
"el_GR": "Χρήση:",
"en_US": "Usage:",
"es_ES": "Uso:",
"fr_FR": "Utilisation :",
"he_IL": "שימוש:",
"it_IT": "Utilizzo:",
"ja_JP": "使用法:",
"ko_KR": "사용법:",
"no_NO": "Forbruk:",
"pl_PL": "Użycie:",
"pt_BR": "Uso:",
"ru_RU": "Применение:",
"sv_SE": "Användning:",
"th_TH": "การใช้งาน:",
"tr_TR": "Kullanım:",
"uk_UA": "Використання:",
"zh_CN": "用法:",
"zh_TW": "用途:"
}
},
{
"ID": "WritableLabel",
"Translations": {
"ar_SA": "الاستخدام (يمكنه حفظ البيانات):",
"de_DE": "Nutzung (Kann Daten speichern):",
"el_GR": "Χρήση (Μπορεί να αποθηκεύσει δεδομένα):",
"en_US": "Usage (Can Save Data):",
"es_ES": "Uso (Puede guardar datos):",
"fr_FR": "Utilisation (Peut enregistrer des données) :",
"he_IL": "שימוש (יכול לשמור נתונים):",
"it_IT": "Utilizzo (Può salvare dati):",
"ja_JP": "使用法(データを保存できます):",
"ko_KR": "사용법 (데이터 저장 가능):",
"no_NO": "Bruk (Kan lagre data):",
"pl_PL": "Użycie (Może zapisywać dane):",
"pt_BR": "Uso (Pode salvar dados):",
"ru_RU": "Применение (Может сохранять данные):",
"sv_SE": "Användning (Kan spara data):",
"th_TH": "การใช้งาน (สามารถบันทึกข้อมูลได้):",
"tr_TR": "Kullanım (Veri kaydedebilir):",
"uk_UA": "Використання (Може зберігати дані):",
"zh_CN": "用法(可保存数据):",
"zh_TW": "用途(可儲存資料):"
}
},
{
"ID": "UnknownLabel",
"Translations": {
"ar_SA": "غير معروف",
"de_DE": "Unbekannt",
"el_GR": "Άγνωστο",
"en_US": "Unknown",
"es_ES": "Desconocido",
"fr_FR": "Inconnu",
"he_IL": "לא ידוע",
"it_IT": "Sconosciuto",
"ja_JP": "不明",
"ko_KR": "알 수 없음",
"no_NO": "Ukjent",
"pl_PL": "Nieznane",
"pt_BR": "Desconhecido",
"ru_RU": "Неизвестно",
"sv_SE": "Okänt",
"th_TH": "ไม่รู้จัก",
"tr_TR": "Bilinmeyen",
"uk_UA": "Невідомо",
"zh_CN": "未知",
"zh_TW": "未知"
}
},
{
"ID": "ScanButton",
"Translations": {
"ar_SA": "فحص Amiibo",
"de_DE": "Amiibo einscannen",
"el_GR": "Σαρώστε το Amiibo",
"en_US": "Scan Amiibo",
"es_ES": "Escanear Amiibo",
"fr_FR": "Scanner l'Amiibo",
"he_IL": "סרוק את ה-Amiibo",
"it_IT": "Scansiona l'Amiibo",
"ja_JP": "Amiiboをスキャン",
"ko_KR": "Amiibo 스캔하기",
"no_NO": "Skanne Amiibo",
"pl_PL": "Zeskanuj Amiibo",
"pt_BR": "Escanear Amiibo",
"ru_RU": "Сканировать Amiibo",
"sv_SE": "Skanna Amiibo",
"th_TH": "สแกน Amiibo",
"tr_TR": "Amiibo'yu tara",
"uk_UA": "Сканувати Amiibo",
"zh_CN": "扫描 Amiibo",
"zh_TW": "掃描 Amiibo"
}
}
]
}
+29
View File
@@ -0,0 +1,29 @@
{
"Locales": [
{
"ID": "Cancel",
"Translations": {
"ar_SA": "إلغاء",
"de_DE": "Abbrechen",
"el_GR": "Ακύρωση",
"en_US": "Cancel",
"es_ES": "Cancelar",
"fr_FR": "Annuler",
"he_IL": "ביטול",
"it_IT": "Annulla",
"ja_JP": "キャンセル",
"ko_KR": "취소",
"no_NO": "Avbryt",
"pl_PL": "Anuluj",
"pt_BR": "Cancelar",
"ru_RU": "Отмена",
"sv_SE": "Avbryt",
"th_TH": "ยกเลิก",
"tr_TR": "İptal",
"uk_UA": "Скасувати",
"zh_CN": "取消",
"zh_TW": "取消"
}
}
]
}
+29
View File
@@ -0,0 +1,29 @@
{
"Locales": [
{
"ID": "AllSupportedFormats",
"Translations": {
"ar_SA": "كل التنسيقات المدعومة",
"de_DE": "Alle unterstützten Formate",
"el_GR": "Όλες Οι Υποστηριζόμενες Μορφές",
"en_US": "All Supported Formats",
"es_ES": "Todos los Formatos Soportados",
"fr_FR": "Tous les Formats Supportés",
"he_IL": "כל הפורמטים הנתמכים",
"it_IT": "Tutti i formati supportati",
"ja_JP": "すべての対応フォーマット",
"ko_KR": "지원되는 모든 형식",
"no_NO": "Alle støttede formater",
"pl_PL": "Wszystkie Obsługiwane Formaty",
"pt_BR": "Todos os Formatos Suportados",
"ru_RU": "Все поддерживаемые форматы",
"sv_SE": "Alla format som stöds",
"th_TH": "รูปแบบที่รองรับทั้งหมด",
"tr_TR": "Tüm Desteklenen Formatlar",
"uk_UA": "Усі підтримувані формати",
"zh_CN": "所有支持的格式",
"zh_TW": "所有支援的格式"
}
}
]
}
+25
View File
@@ -1,5 +1,30 @@
{
"Locales": [
{
"ID": "SearchAmiiboWatermark",
"Translations": {
"ar_SA": "بحث Amiibo...",
"de_DE": "Amiibo suchen...",
"el_GR": "Αναζήτηση Amiibo...",
"en_US": "Search Amiibo...",
"es_ES": "Buscar Amiibo...",
"fr_FR": "Rechercher Amiibo...",
"he_IL": "חפש Amiibo...",
"it_IT": "Cerca Amiibo...",
"ja_JP": "Amiiboを検索...",
"ko_KR": "Amiibo 검색...",
"no_NO": "Søk Amiibo...",
"pl_PL": "Wyszukaj Amiibo...",
"pt_BR": "Buscar Amiibo...",
"ru_RU": "Поиск Amiibo...",
"sv_SE": "Sök Amiibo...",
"th_TH": "ค้นหา Amiibo...",
"tr_TR": "Amiibo Ara...",
"uk_UA": "Пошук Amiibo...",
"zh_CN": "搜索 Amiibo...",
"zh_TW": "搜尋 Amiibo..."
}
},
{
"ID": "SearchWatermark",
"Translations": {
+1 -1
View File
@@ -54,7 +54,7 @@
"ID": "TrimStatusLabel",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Trimmstatus",
"el_GR": "",
"en_US": "Trim Status",
"es_ES": "Estado del recorte",
+179
View File
@@ -0,0 +1,179 @@
{
"Locales": [
{
"ID": "ScanAmiiboFromBinFilePickerTitle",
"Translations": {
"ar_SA": "",
"de_DE": "Wähle eine BIN-Datei aus, um ein Amiibo zu scannen.",
"el_GR": "",
"en_US": "Select a BIN file to scan an Amiibo from",
"es_ES": "Selecciona un archivo BIN para escanear un Amiibo",
"fr_FR": "Sélectionnez un fichier BIN pour scanner un Amiibo",
"he_IL": "",
"it_IT": "Seleziona un file BIN da cui scansionare un Amiibo",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "Selecione um arquivo BIN para escanear um Amiibo",
"ru_RU": "Выберите BIN-файл для сканирования Amiibo",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "Виберіть BIN-файл для сканування Amiibo",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "APITitle",
"Translations": {
"ar_SA": "أميبو API",
"de_DE": "Amiibo-API",
"el_GR": "API για Amiibo.",
"en_US": "Amiibo API",
"es_ES": "API Amiibo",
"fr_FR": "API Amiibo",
"he_IL": "ממשק תכנות אמיבו",
"it_IT": "API Amiibo",
"ja_JP": "",
"ko_KR": null,
"no_NO": "",
"pl_PL": "API Amiibo",
"pt_BR": "API Amiibo",
"ru_RU": "API Amiibo",
"sv_SE": "Amiibo-API",
"th_TH": "อามิโบ้ API",
"tr_TR": "",
"uk_UA": "API Amiibo",
"zh_CN": null,
"zh_TW": null
}
},
{
"ID": "APIFailFetchMessage",
"Translations": {
"ar_SA": "حدث خطأ أثناء جلب المعلومات من API.",
"de_DE": "Beim Abrufen von Informationen aus der API ist ein Fehler aufgetreten.",
"el_GR": "Παρουσιάστηκε σφάλμα κατά την ανάκτηση πληροφοριών από το API.",
"en_US": "An error occured while fetching information from the API.",
"es_ES": "Ocurrió un error al recibir información de la API.",
"fr_FR": "Une erreur est survenue lors de la récupération des informations de l'API.",
"he_IL": "אירעה שגיאה בעת שליפת מידע מהממשק.",
"it_IT": "Si è verificato un errore durante il recupero delle informazioni dall'API.",
"ja_JP": "API からの情報取得中にエラーが発生しました.",
"ko_KR": "API에서 정보를 가져오는 중에 오류가 발생했습니다.",
"no_NO": "En feil oppstod under henting av informasjon fra API.",
"pl_PL": "Wystąpił błąd podczas pobierania informacji z API.",
"pt_BR": "Um erro ocorreu ao tentar obter informações da API.",
"ru_RU": "Произошла ошибка при получении информации из API.",
"sv_SE": "Ett fel inträffade vid hämtning av information från API.",
"th_TH": "เกิดข้อผิดพลาดขณะเรียกข้อมูลจาก API",
"tr_TR": "API'dan bilgi alırken bir hata oluştu.",
"uk_UA": "Під час отримання інформації з API сталася помилка.",
"zh_CN": "从 API 获取信息时出错。",
"zh_TW": "從 API 取得資訊時出現錯誤。"
}
},
{
"ID": "APIConnectErrorMessage",
"Translations": {
"ar_SA": "غير قادر على الاتصال بخادم API أميبو. قد تكون الخدمة معطلة أو قد تحتاج إلى التحقق من اتصالك بالإنترنت.",
"de_DE": "Verbindung zum Amiibo API Server kann nicht hergestellt werden. Der Dienst ist möglicherweise nicht verfügbar oder es existiert keine Internetverbindung.",
"el_GR": "Δεν είναι δυνατή η σύνδεση με τον διακομιστή Amiibo API. Η υπηρεσία μπορεί να είναι εκτός λειτουργίας ή μπορεί να χρειαστεί να επαληθεύσετε ότι έχετε ενεργή σύνδεσή στο Διαδίκτυο.",
"en_US": "Unable to connect to Amiibo API server. The service may be down or you may need to verify your internet connection is online.",
"es_ES": "No se pudo conectar al servidor de la API Amiibo. El servicio puede estar caído o tu conexión a internet puede haberse desconectado.",
"fr_FR": "Impossible de se connecter au serveur API Amiibo. Le service est peut-être hors service ou vous devriez peut-être vérifier que votre connexion internet est connectée.",
"he_IL": "לא ניתן להתחבר לממשק שרת האמיבו. ייתכן שהשירות מושבת או שתצטרך לוודא שהחיבור לאינטרנט שלך מקוון.",
"it_IT": "Impossibile connettersi al server dell'API Amiibo. Il servizio potrebbe non essere disponibile o potresti non essere connesso a Internet.",
"ja_JP": "Amiibo API サーバに接続できませんでした. サーバがダウンしているか, インターネット接続に問題があるかもしれません.",
"ko_KR": "Amiibo API 서버에 연결할 수 없습니다. 서비스가 다운되었거나 인터넷 연결이 온라인 상태인지 확인이 필요합니다.",
"no_NO": "Kan ikke koble til Amiibo API server. Tjenesten kan være nede, eller du må kanskje verifisere at din internettforbindelse er tilkoblet.",
"pl_PL": "Nie można połączyć się z serwerem API Amiibo. Usługa może nie działać lub może być konieczne sprawdzenie, czy połączenie internetowe jest online.",
"pt_BR": "Não foi possível conectar ao servidor da API Amiibo. O serviço pode estar fora do ar ou você precisa verificar sua conexão com a Internet.",
"ru_RU": "Не удалось подключиться к серверу Amiibo API. Служба может быть недоступна или вам может потребоваться проверить ваше интернет-соединение.",
"sv_SE": "Kunde inte ansluta till Amiibo API-server. Tjänsten kanske är nere eller så behöver du kontrollera att din internetanslutning fungerar.",
"th_TH": "ไม่สามารถเชื่อมต่อกับเซิร์ฟเวอร์ Amiibo API บางบริการอาจหยุดทำงาน หรือไม่คุณต้องทำการตรวจสอบว่าอินเทอร์เน็ตของคุณอยู่ในสถานะเชื่อมต่ออยู่หรือไม่",
"tr_TR": "Amiibo API sunucusuna bağlanılamadı. Sunucu çevrimdışı olabilir veya uygun bir internet bağlantınızın olduğunu kontrol etmeniz gerekebilir.",
"uk_UA": "Неможливо підключитися до сервера Amiibo API. Можливо, служба не працює або вам потрібно перевірити, чи є підключення до Інтернету.",
"zh_CN": "无法连接到 Amiibo API 服务器,服务可能已关闭,或者没有连接互联网。",
"zh_TW": "無法連接 Amiibo API 伺服器。服務可能已停機,或者您可能需要確認網際網路連線是否在線上。"
}
},
{
"ID": "RenameAmiiboTitle",
"Translations": {
"ar_SA": "",
"de_DE": "Amiibo umbenennen",
"el_GR": "",
"en_US": "Rename Amiibo",
"es_ES": "Renombrar Amiibo",
"fr_FR": "Renommer l'Amiibo",
"he_IL": "",
"it_IT": "Rinomina Amiibo",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "Renomear Amiibo",
"ru_RU": "Переименовать Amiibo",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "Перейменувати Amiibo",
"zh_CN": "",
"zh_TW": ""
}
},
{
"ID": "ScanAmiiboTitle",
"Translations": {
"ar_SA": "فحص Amiibo",
"de_DE": "Amiibo einscannen",
"el_GR": "Σαρώστε το Amiibo",
"en_US": "Scan Amiibo",
"es_ES": "Escanear Amiibo",
"fr_FR": "Scanner l'Amiibo",
"he_IL": "סרוק את ה-Amiibo",
"it_IT": "Scansiona l'Amiibo",
"ja_JP": "Amiiboをスキャン",
"ko_KR": "Amiibo 스캔하기",
"no_NO": "Skanne Amiibo",
"pl_PL": "Zeskanuj Amiibo",
"pt_BR": "Escanear Amiibo",
"ru_RU": "Сканировать Amiibo",
"sv_SE": "Skanna Amiibo",
"th_TH": "สแกน Amiibo",
"tr_TR": "Amiibo'yu tara",
"uk_UA": "Сканувати Amiibo",
"zh_CN": "扫描 Amiibo",
"zh_TW": "掃描 Amiibo"
}
},
{
"ID": "ScanAmiiboMessage",
"Translations": {
"ar_SA": "",
"de_DE": "Bitte scanne jetzt dein Amiibo.",
"el_GR": "",
"en_US": "Please scan your Amiibo now.",
"es_ES": "Escanea tu Amiibo ahora.",
"fr_FR": "Veuillez scannez votre Amiibo.",
"he_IL": "",
"it_IT": "Scansiona ora il tuo Amiibo.",
"ja_JP": "",
"ko_KR": "지금 Amiibo를 스캔하세요.",
"no_NO": "Vennligst skann Amiiboene dine nå.",
"pl_PL": "",
"pt_BR": "Por favor, escaneie seu Amiibo agora.",
"ru_RU": "Пожалуйста, сканируйте ваш Amiibo.",
"sv_SE": "Skanna din Amiibo nu.",
"th_TH": "กรุณาสแกน Amiibo ของคุณตอนนี้",
"tr_TR": "",
"uk_UA": "Будь ласка, проскануйте Ваш Amiibo.",
"zh_CN": "请现在扫描你的 Amiibo",
"zh_TW": "請掃描你的 Amiibo。"
}
}
]
}
+4 -4
View File
@@ -4,7 +4,7 @@
"ID": "UpdatesAddedMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Updates hinzugefügt: {0}",
"el_GR": "",
"en_US": "Updates Added: {0}",
"es_ES": "Actualizaciones Añadidas: {0}",
@@ -29,7 +29,7 @@
"ID": "UpdatesRemovedMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Updates entfernt: {0}",
"el_GR": "",
"en_US": "Updates Removed: {0}",
"es_ES": "Actualizaciones Eliminadas: {0}",
@@ -54,7 +54,7 @@
"ID": "DLCAddedMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "DLC hinzugefügt: {0}",
"el_GR": "",
"en_US": "DLC Added: {0}",
"es_ES": "DLC Añadidos: {0}",
@@ -79,7 +79,7 @@
"ID": "DLCRemovedMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "DLC entfernt: {0}",
"el_GR": "",
"en_US": "DLC Removed: {0}",
"es_ES": "DLC Eliminados: {0}",
+3 -3
View File
@@ -4,7 +4,7 @@
"ID": "LoadApplicationFromFileFilePickerTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Wähle eine Switch-Anwendungsdatei zum Laden aus",
"el_GR": "",
"en_US": "Select a Switch application file to load",
"es_ES": "Selecciona un archivo de aplicación de Switch para cargar",
@@ -29,7 +29,7 @@
"ID": "LoadUnpackedApplicationFromFolderFilePickerTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Wähle einen Ordner mit einer entpackten Switch-Anwendung zum Laden",
"el_GR": "",
"en_US": "Select a folder containing an unpacked Switch application to load",
"es_ES": "Selecciona una carpeta que contenga una aplicación de Switch descomprimida para cargar",
@@ -54,7 +54,7 @@
"ID": "LoadUpdatesAndDLCFromFolderFilePickerTitle",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Wähle einen oder mehrere Ordner zum Laden von Updates und DLC aus",
"el_GR": "",
"en_US": "Select one or more folders to bulk load updates and DLC from",
"es_ES": "Selecciona una o más carpetas para cargar de forma masiva actualizaciones y DLC",
+29
View File
@@ -0,0 +1,29 @@
{
"Locales": [
{
"ID": "ScanSkylanderFilePickerTitle",
"Translations": {
"ar_SA": "",
"de_DE": "Wähle eine BIN-, SKY-, DMP- oder DUMP-Datei aus, um einen Skylander zu scannen.",
"el_GR": "",
"en_US": "Select a BIN, SKY, DMP, or DUMP file to scan a Skylander from",
"es_ES": "Selecciona un archivo BIN, SKY, DMP o DUMP para escanear un Skylander",
"fr_FR": "Sélectionnez un fichier BIN, SKY, DMP ou DUMP pour scanner un Skylander",
"he_IL": "",
"it_IT": "Seleziona un file BIN, SKY, DMP o DUMP da cui scansionare uno Skylander",
"ja_JP": "",
"ko_KR": "",
"no_NO": "",
"pl_PL": "",
"pt_BR": "Selecione um arquivo BIN, SKY, DMP ou DUMP para escanear um Skylander",
"ru_RU": "Выберите файл BIN, SKY, DMP или DUMP для сканирования Skylander",
"sv_SE": "",
"th_TH": "",
"tr_TR": "",
"uk_UA": "Виберіть файл BIN, SKY, DMP або DUMP для сканування Skylander",
"zh_CN": "",
"zh_TW": ""
}
}
]
}
+20 -20
View File
@@ -4,7 +4,7 @@
"ID": "PrimaryMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Entfernt ungenutzten Speicherplatz aus der XCI-Datei, um ihre Dateigröße zu verringern.",
"el_GR": "",
"en_US": "Removes unused space from the XCI to reduce its file size.",
"es_ES": "Elimina el espacio no utilizado del XCI para reducir su tamaño.",
@@ -29,7 +29,7 @@
"ID": "SecondaryMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Datei: {0:n} MB • Spiel: {1:n} MB\n\nErsparnis: {2:n} MB",
"el_GR": "",
"en_US": "File: {0:n} MB • Game: {1:n} MB\n\nSavings: {2:n} MB",
"es_ES": "Archivo: {0:n} MB • Juego: {1:n} MB\n\nAhorro: {2:n} MB",
@@ -47,18 +47,18 @@
"tr_TR": "",
"uk_UA": "Файл: {0:n} МБ • Гра: {1:n} МБ\n\nЕкономія: {2:n} МБ",
"zh_CN": "文件: {0:n} MB • 游戏: {1:n} MB\n\n节省: {2:n} MB",
"zh_TW": "檔案: {0:n} MB • 遊戲: {1:n} MB\n\n節省: {2:n} MB"
"zh_TW": "檔案: {0:n} MB • 遊戲: {1:n} MB\n\n節省: {2:n} MB"
}
},
{
"ID": "NoTrimNecessaryMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "XCI muss nicht getrimmt werden. Prüfen Sie die Logs für Details.",
"el_GR": "",
"en_US": "XCI does not require trimming. Check logs for details.",
"es_ES": "El XCI no necesita ser recortado. Verifica los logs para detalles.",
"fr_FR": "Le XCI na pas besoin d’être réduit. Référez-vous aux journaux pour détails.",
"fr_FR": "Le XCI na pas besoin d’être réduit. Référez-vous aux journaux pour détails.",
"he_IL": "",
"it_IT": "Non è necessario ridurre la dimensione del XCI. Controlla i log per dettagli.",
"ja_JP": "",
@@ -79,7 +79,7 @@
"ID": "NoUntrimPossibleMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Das Trimmen der XCI kann nicht rückgängig gemacht werden. Prüfen Sie die Logs für Details.",
"el_GR": "",
"en_US": "XCI cannot be untrimmed. Check logs for details.",
"es_ES": "El recorte del XCI no puede ser deshecho. Verifica los registros para detalles.",
@@ -104,7 +104,7 @@
"ID": "ReadOnlyFileCannotFixMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "XCI ist schreibgeschützt und konnte nicht schreibbar gemacht werden. Prüfen Sie die Logs für Details.",
"el_GR": "",
"en_US": "XCI is read-only and could not be made writable. Check logs for details.",
"es_ES": "XCI es solo lectura y no se puede escribir. Verifica los registros para detalles.",
@@ -120,7 +120,7 @@
"sv_SE": "XCI-filen är skrivskyddad och kunde inte göras skrivbar. Kontrollera loggen för mer information",
"th_TH": "XCI เป็นอ่านอย่างเดียวและไม่สามารถเขียนได้ ตรวจสอบบันทึกสำหรับรายละเอียด",
"tr_TR": "",
"uk_UA": "XCI тільки для читання і не можна записати. Перевірте логи для деталей.",
"uk_UA": "XCI тільки для читання і не можна записати. Перевірте логи для деталей.",
"zh_CN": "XCI 只读,无法写入。查看日志以获取详情。",
"zh_TW": "XCI 檔案唯讀,無法寫入。檢查日誌以取得詳情。"
}
@@ -129,11 +129,11 @@
"ID": "SizeChangedMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "XCI-Größe hat sich seit dem letzten Scan geändert. Stellen Sie sicher, dass die Datei nicht beschrieben wird, und versuchen Sie es erneut.",
"el_GR": "",
"en_US": "XCI size changed since last scan. Ensure the file is not being written to and try again.",
"es_ES": "El tamaño de XCI ha cambiado desde que fue escaneado. Verifica que no se esté escribiendo al archivo y vuelve a intentarlo.",
"fr_FR": "La taille de XCI a changé depuis son analyse. Vérifiez que le fichier nest pas en cours d’écriture, puis réessayez.",
"fr_FR": "La taille de XCI a changé depuis son analyse. Vérifiez que le fichier nest pas en cours d’écriture, puis réessayez.",
"he_IL": "",
"it_IT": "La dimensione di XCI è cambiata da quando è stato scansionato. Controlla che il file non sia scritto e riprova.",
"ja_JP": "",
@@ -154,7 +154,7 @@
"ID": "FreeSpaceCheckFailedMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "XCI enthält Daten im freien Speicherbereich. Es ist unsicher zu trimmen.",
"el_GR": "",
"en_US": "XCI has data in the free space area. It is not safe to trim.",
"es_ES": "XCI tiene datos en el área de espacio libre. No es seguro recortarlo.",
@@ -166,7 +166,7 @@
"no_NO": "XCI har data i den ledige plassen. Det er ikke trygt å trimme den.",
"pl_PL": "XCI zawiera dane w obszarze wolnego miejsca. Nie jest bezpieczne go przycinać.",
"pt_BR": "XCI tem dados na área de espaço livre. Não é seguro reduzi-lo.",
"ru_RU": "XCI содержит данные в свободной области. Его обрезка небезопасна.",
"ru_RU": "XCI содержит данные в свободной области. Его обрезка небезопасна.",
"sv_SE": "XCI har data i det lediga utrymmet. Det är inte säkert att optimera.",
"th_TH": "XCI มีข้อมูลในพื้นที่ว่าง จึงไม่ปลอดภัยที่จะทำการตัดแต่ง",
"tr_TR": "",
@@ -179,7 +179,7 @@
"ID": "InvalidDataMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "XCI enthält ungültige Daten. Prüfen Sie die Logs für Details.",
"el_GR": "",
"en_US": "XCI contains invalid data. Check logs for details.",
"es_ES": "XCI contiene datos inválidos. Lee el registro para detalles.",
@@ -204,7 +204,7 @@
"ID": "WriteErrorMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "XCI konnte nicht zum Schreiben geöffnet werden. Prüfen Sie die Logs für Details.",
"el_GR": "",
"en_US": "XCI could not be opened for writing. Check logs for details.",
"es_ES": "XCI no se puede abrir para escribir. Lee el registro para detalles.",
@@ -216,11 +216,11 @@
"no_NO": "XCI kunne ikke åpnes for skriving. Sjekk loggene for detaljer.",
"pl_PL": "Nie można otworzyć XCI do zapisu. Sprawdź dzienniki, aby uzyskać szczegóły.",
"pt_BR": "XCI não pôde ser aberto para gravação. Verifique os logs para detalhes.",
"ru_RU": "Не удалось открыть XCI для записи. Проверьте журналы для подробностей.",
"ru_RU": "Не удалось открыть XCI для записи. Проверьте журналы для подробностей.",
"sv_SE": "XCI kunde inte öppnas för skrivning. Kontrollera loggen för detaljer.",
"th_TH": "ไม่สามารถเปิด XCI เพื่อเขียนข้อมูลได้ โปรดตรวจสอบบันทึกสำหรับรายละเอียด",
"tr_TR": "",
"uk_UA": "Не вдалося відкрити XCI для запису. Перевірте журнали для деталей.",
"uk_UA": "Не вдалося відкрити XCI для запису. Перевірте журнали для деталей.",
"zh_CN": "XCI 不能写入。查看日志以获得更多细节。",
"zh_TW": "XCI 無法開啟以進行寫入。請檢查日誌以取得更多資訊。"
}
@@ -229,7 +229,7 @@
"ID": "TrimFailedMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Trimmen der XCI fehlgeschlagen.",
"el_GR": "",
"en_US": "Failed to trim XCI.",
"es_ES": "El recorte del XCI falló.",
@@ -254,7 +254,7 @@
"ID": "TrimCancelledMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Der Vorgang wurde abgebrochen.",
"el_GR": "",
"en_US": "The operation was cancelled.",
"es_ES": "La operación fue cancelada.",
@@ -279,7 +279,7 @@
"ID": "NoOperationPerformedMessage",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Es wurde keine Aktion durchgeführt.",
"el_GR": "",
"en_US": "No operation was performed.",
"es_ES": "No se realizó ninguna operación.",
@@ -301,4 +301,4 @@
}
}
]
}
}
+1 -1
View File
@@ -29,7 +29,7 @@
"ID": "NoUnpackedApplicationFoundInFolder",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Bitte wähle einen entpackten Anwendungsordner mit gültigem ExeFS oder NSO/NRO aus.",
"el_GR": "",
"en_US": "Please select an unpacked application folder with a valid ExeFS or NSO/NRO.",
"es_ES": "",
+3 -3
View File
@@ -3,7 +3,7 @@
{
"ID": "TrimXCIButton",
"Translations": {
"ar_SA": "تقليم XCI",
"ar_SA": "تقليم XCI",
"de_DE": "Zuschneiden der XCI",
"el_GR": "Κοπή XCI",
"en_US": "Trim XCI",
@@ -19,11 +19,11 @@
"ru_RU": "Обрезать XCI",
"sv_SE": "Optimera XCI",
"th_TH": "ลดขนาด XCI",
"tr_TR": "XCI'yi Kırp",
"tr_TR": "XCI'yi Kırp",
"uk_UA": "Нарізка XCI",
"zh_CN": "精简 XCI",
"zh_TW": "修剪 XCI"
}
}
]
}
}
+21 -21
View File
@@ -228,7 +228,7 @@
{
"ID": "XCITrimmerButton",
"Translations": {
"ar_SA": "",
"ar_SA": "",
"de_DE": "XCI-Trimmer",
"el_GR": "",
"en_US": "XCI Trimmer",
@@ -378,26 +378,26 @@
{
"ID": "ScanAmiiboFromBinButton",
"Translations": {
"ar_SA": "مسح Amiibo (.BIN)",
"de_DE": "Amiibo scannen (.BIN)",
"el_GR": "Σάρωση Amiibo (.BIN)",
"en_US": "Scan Amiibo (.BIN)",
"es_ES": "Escanear un Amiibo (.BIN)",
"fr_FR": "Scanner un Amiibo (.BIN)",
"he_IL": "סרוק Amiibo (.BIN)",
"it_IT": "Scansiona un Amiibo (.BIN)",
"ja_JP": "Amiibo をスキャン (.BIN)",
"ko_KR": "Amiibo 스캔 (.BIN)",
"no_NO": "Skann en Amiibo (.BIN)",
"pl_PL": "Skanuj Amiibo (.BIN)",
"pt_BR": "Escaneie um Amiibo (.BIN)",
"ru_RU": "Сканировать Amiibo (.BIN)",
"sv_SE": "Skanna en Amiibo (.BIN)",
"th_TH": "สแกนอามีโบ (.BIN)",
"tr_TR": "Amiibo Tara (.BIN)",
"uk_UA": "Сканувати Amiibo (.BIN)",
"zh_CN": "扫描 Amiibo (.BIN)",
"zh_TW": "掃瞄 Amiibo (.BIN)"
"ar_SA": "مسح Amiibo (BIN)",
"de_DE": "Amiibo scannen (BIN)",
"el_GR": "Σάρωση Amiibo (BIN)",
"en_US": "Scan Amiibo (BIN)",
"es_ES": "Escanear un Amiibo (BIN)",
"fr_FR": "Scanner un Amiibo (BIN)",
"he_IL": "סרוק Amiibo (BIN)",
"it_IT": "Scansiona un Amiibo (BIN)",
"ja_JP": "Amiibo をスキャン (BIN)",
"ko_KR": "Amiibo 스캔 (BIN)",
"no_NO": "Skann en Amiibo (BIN)",
"pl_PL": "Skanuj Amiibo (BIN)",
"pt_BR": "Escaneie um Amiibo (BIN)",
"ru_RU": "Сканировать Amiibo (BIN)",
"sv_SE": "Skanna en Amiibo (BIN)",
"th_TH": "สแกนอามีโบ (BIN)",
"tr_TR": "Amiibo Tara (BIN)",
"uk_UA": "Сканувати Amiibo (BIN)",
"zh_CN": "扫描 Amiibo (BIN)",
"zh_TW": "掃瞄 Amiibo (BIN)"
}
},
{
+4 -4
View File
@@ -154,7 +154,7 @@
"ID": "OpenScreenshotsFolderButton",
"Translations": {
"ar_SA": "",
"de_DE": "Screenshots-Ordner öffnen",
"de_DE": "Bildschirmfoto-Ordner öffnen",
"el_GR": "",
"en_US": "Open Screenshots Folder",
"es_ES": "Abrir Carpeta de Capturas de Pantalla",
@@ -179,7 +179,7 @@
"ID": "AssociateFileTypesButton",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Dateitypen zuordnen",
"el_GR": "",
"en_US": "Associate File Types",
"es_ES": "",
@@ -204,7 +204,7 @@
"ID": "RemoveFileTypeAssociationsButton",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Dateityp-Zuordnungen entfernen",
"el_GR": "",
"en_US": "Remove File Type Associations",
"es_ES": "",
@@ -229,7 +229,7 @@
"ID": "FileTypeAssociationsToolTip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Ordnet unterstützte Dateitypen (NSP, XCI, NCA, NRO, NSO) Ryujinx zu, sodass sie beim Doppelklick im Dateimanager automatisch geöffnet werden. Das Entfernen der Dateityp-Zuordnungen deaktiviert dieses Verhalten.",
"el_GR": "",
"en_US": "Associates supported file types (NSP, XCI, NCA, NRO, NSO) with Ryujinx so they open automatically when double-clicked in your file manager. Removing file type associations stops this behavior.",
"es_ES": "",
+168 -868
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -54,7 +54,7 @@
"ID": "StartGamesWithoutUICheckboxLabelToolTip",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Blendet die Benutzeroberfläche von Ryujinx automatisch aus, sobald ein Spiel gestartet wird. Während des Spiels kann die Oberfläche mit F4 eingeblendet werden.",
"el_GR": "",
"en_US": "Automatically hides Ryujinx's UI whenever a game launches. While in-game, press F4 to show the UI.",
"es_ES": "",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"Locales": [
{
{
"ID": "TrimmingXCILabel",
"Translations": {
"ar_SA": "جاري تقليم: {0}",
@@ -19,7 +19,7 @@
"ru_RU": "Обрезка: {0}",
"sv_SE": "Trimmar: {0}",
"th_TH": "กำลังตัด: {0}",
"tr_TR": "{0} Kısaltılıyor:",
"tr_TR": "{0} Kısaltılıyor:",
"uk_UA": "Обрізка: {0}",
"zh_CN": "正在修剪: {0}",
"zh_TW": "正在修剪: {0}"
+14 -14
View File
@@ -4,7 +4,7 @@
"ID": "StatusCountLabel",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "XCI ausgewählt: {0}/{1}",
"el_GR": "",
"en_US": "XCI Selected: {0}/{1}",
"es_ES": "XCI Seleccionados: {0}/{1}",
@@ -29,7 +29,7 @@
"ID": "StatusCountWithFilterLabel",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "XCI ausgewählt: {0}/{1} (Sichtbar: {2})",
"el_GR": "",
"en_US": "XCI Selected: {0}/{1} (Displayed: {2})",
"es_ES": "XCI Seleccionados: {0}/{1} (Mostrados: {2})",
@@ -48,13 +48,13 @@
"uk_UA": "Вибрано XCI: {0}/{1} (Відображається: {2})",
"zh_CN": "XCI 已选: {0}/{1}(显示: {2}",
"zh_TW": "XCI 已選擇: {0}/{1}(顯示: {2}"
}
}
},
{
"ID": "StatusTrimmingLabel",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Trimmen: {0}/{1}...",
"el_GR": "",
"en_US": "Trimming: {0}/{1}...",
"es_ES": "Recortando: {0}/{1}...",
@@ -79,7 +79,7 @@
"ID": "StatusUntrimmingLabel",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Trimmung von {0} wieder aufheben...",
"el_GR": "",
"en_US": "Untrimming {0}...",
"es_ES": "Deshaciendo el recorte {0}...",
@@ -103,7 +103,7 @@
{
"ID": "SelectAllButton",
"Translations": {
"ar_SA": "اختر الكل",
"ar_SA": "اختر الكل",
"de_DE": "Alles auswählen",
"el_GR": "Επιλογή όλων",
"en_US": "Select All",
@@ -154,7 +154,7 @@
"ID": "TrimmedLabel",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Getrimmt",
"el_GR": "",
"en_US": "Trimmed",
"es_ES": "Recortado",
@@ -179,7 +179,7 @@
"ID": "UntrimmedLabel",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Ungetrimmt",
"el_GR": "",
"en_US": "Untrimmed",
"es_ES": "Sin Recortar",
@@ -204,7 +204,7 @@
"ID": "PartialLabel",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Teilweise",
"el_GR": "",
"en_US": "Partial",
"es_ES": "Parcial",
@@ -229,7 +229,7 @@
"ID": "FailedLabel",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Fehlgeschlagen",
"el_GR": "",
"en_US": "Failed",
"es_ES": "Fallido",
@@ -304,7 +304,7 @@
"ID": "SavedLabel",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Gespeichert:",
"el_GR": "",
"en_US": "Saved:",
"es_ES": "Ahorrado:",
@@ -366,7 +366,7 @@
"no_NO": null,
"pl_PL": null,
"pt_BR": null,
"ru_RU": "{0} МБ",
"ru_RU": "{0} МБ",
"sv_SE": null,
"th_TH": null,
"tr_TR": null,
@@ -379,7 +379,7 @@
"ID": "TrimButton",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Trimmen",
"el_GR": "",
"en_US": "Trim",
"es_ES": "Recortar",
@@ -404,7 +404,7 @@
"ID": "UntrimButton",
"Translations": {
"ar_SA": "",
"de_DE": "",
"de_DE": "Trimmen aufheben",
"el_GR": "",
"en_US": "Untrim",
"es_ES": "Desrecortar",
+2 -1
View File
@@ -2667,7 +2667,8 @@
0100C52011460000,"Sky: Children of the Light",cpu;online-broken,nothing,2023-02-23 10:57:10
010041C01014E000,"Skybolt Zack",,playable,2021-04-12 18:28:00
0100A0A00D1AA000,"SKYHILL",,playable,2021-03-05 15:19:11
0100CCC0002E6000,"Skylanders Imaginators",crash;services,boots,2020-05-30 18:49:18
010048D000BFC000,"Skylanders Imaginators",,playable,2026-07-02 22:43:30
0100CCC0002E6000,"Skylanders Imaginators",,playable,2026-07-02 22:43:30
010021A00ABEE000,"SKYPEACE",,playable,2020-05-29 14:14:30
0100EA400BF44000,"SkyScrappers",,playable,2020-05-28 22:11:25
0100F3C00C400000,"SkyTime",slow,ingame,2020-05-30 09:24:51
1 title_id game_name labels status last_updated
2667 0100C52011460000 Sky: Children of the Light cpu;online-broken nothing 2023-02-23 10:57:10
2668 010041C01014E000 Skybolt Zack playable 2021-04-12 18:28:00
2669 0100A0A00D1AA000 SKYHILL playable 2021-03-05 15:19:11
2670 0100CCC0002E6000 010048D000BFC000 Skylanders Imaginators crash;services boots playable 2020-05-30 18:49:18 2026-07-02 22:43:30
2671 0100CCC0002E6000 Skylanders Imaginators playable 2026-07-02 22:43:30
2672 010021A00ABEE000 SKYPEACE playable 2020-05-29 14:14:30
2673 0100EA400BF44000 SkyScrappers playable 2020-05-28 22:11:25
2674 0100F3C00C400000 SkyTime slow ingame 2020-05-30 09:24:51
+7 -5
View File
@@ -8,7 +8,7 @@ namespace ARMeilleure.CodeGen
/// <summary>
/// Represents a compiled function.
/// </summary>
readonly struct CompiledFunction
public readonly struct CompiledFunction
{
/// <summary>
/// Gets the machine code of the <see cref="CompiledFunction"/>.
@@ -44,10 +44,11 @@ namespace ARMeilleure.CodeGen
/// <typeparamref name="T"/> pointing to the mapped function.
/// </summary>
/// <typeparam name="T">Type of delegate</typeparam>
/// <param name="jitCache">The jit cache to map the function into</param>
/// <returns>A delegate of type <typeparamref name="T"/> pointing to the mapped function</returns>
public T Map<T>()
public T Map<T>(JitCache jitCache)
{
return MapWithPointer<T>(out _);
return MapWithPointer<T>(jitCache, out _);
}
/// <summary>
@@ -55,11 +56,12 @@ namespace ARMeilleure.CodeGen
/// <typeparamref name="T"/> pointing to the mapped function.
/// </summary>
/// <typeparam name="T">Type of delegate</typeparam>
/// <param name="jitCache">The jit cache to map the function into</param>
/// <param name="codePointer">Pointer to the function code in memory</param>
/// <returns>A delegate of type <typeparamref name="T"/> pointing to the mapped function</returns>
public T MapWithPointer<T>(out nint codePointer)
public T MapWithPointer<T>(JitCache jitCache, out nint codePointer)
{
codePointer = JitCache.Map(this);
codePointer = jitCache.Map(this);
return Marshal.GetDelegateForFunctionPointer<T>(codePointer);
}
@@ -3,7 +3,7 @@ namespace ARMeilleure.CodeGen.Linking
/// <summary>
/// Represents a relocation.
/// </summary>
readonly struct RelocEntry
public readonly struct RelocEntry
{
public const int Stride = 13; // Bytes.
+1 -1
View File
@@ -5,7 +5,7 @@ namespace ARMeilleure.CodeGen.Linking
/// <summary>
/// Represents relocation information about a <see cref="CompiledFunction"/>.
/// </summary>
readonly struct RelocInfo
public readonly struct RelocInfo
{
/// <summary>
/// Gets an empty <see cref="RelocInfo"/>.
+1 -1
View File
@@ -5,7 +5,7 @@ namespace ARMeilleure.CodeGen.Linking
/// <summary>
/// Represents a symbol.
/// </summary>
readonly struct Symbol
public readonly struct Symbol
{
private readonly ulong _value;
@@ -3,7 +3,7 @@ namespace ARMeilleure.CodeGen.Linking
/// <summary>
/// Types of <see cref="Symbol"/>.
/// </summary>
enum SymbolType : byte
public enum SymbolType : byte
{
/// <summary>
/// Refers to nothing, i.e no symbol.
@@ -1,6 +1,6 @@
namespace ARMeilleure.CodeGen.Unwinding
{
struct UnwindInfo
public struct UnwindInfo
{
public const int Stride = 4; // Bytes.
@@ -1,6 +1,6 @@
namespace ARMeilleure.CodeGen.Unwinding
{
enum UnwindPseudoOp
public enum UnwindPseudoOp
{
PushReg = 0,
SetFrame = 1,
@@ -1,6 +1,6 @@
namespace ARMeilleure.CodeGen.Unwinding
{
struct UnwindPushEntry
public struct UnwindPushEntry
{
public const int Stride = 16; // Bytes.
+1 -1
View File
@@ -6,7 +6,7 @@ namespace ARMeilleure.Common
public readonly struct AddressTableLevel
{
/// <summary>
/// Gets the index of the <see cref="Level"/> in the guest address.
/// Gets the index of the <see cref="Index"/> in the guest address.
/// </summary>
public int Index { get; }
+8 -32
View File
@@ -19,46 +19,22 @@ namespace ARMeilleure.Common
new( 7, 8),
new( 1, 6)
];
private static readonly AddressTableLevel[] _levels64BitSparseTiny =
private static readonly AddressTableLevel[] _monoSparse64Bit =
[
new( 11, 28),
new( 2, 9)
new( 2, 37)
];
private static readonly AddressTableLevel[] _levels32BitSparseTiny =
private static readonly AddressTableLevel[] _monoSparse32Bit =
[
new( 10, 22),
new( 1, 9)
new( 1, 31)
];
private static readonly AddressTableLevel[] _levels64BitSparseGiant =
[
new( 38, 1),
new( 2, 36)
];
private static readonly AddressTableLevel[] _levels32BitSparseGiant =
[
new( 31, 1),
new( 1, 30)
];
//high power will run worse on DDR3 systems and some DDR4 systems due to the higher ram utilization
//low power will never run worse than non-sparse, but for most systems it won't be necessary
//high power is always used, but I've left low power in here for future reference
public static AddressTableLevel[] GetArmPreset(bool for64Bits, bool sparse, bool lowPower = false)
public static AddressTableLevel[] GetArmPreset(bool for64Bits, bool sparse)
{
if (sparse)
{
if (lowPower)
{
return for64Bits ? _levels64BitSparseTiny : _levels32BitSparseTiny;
}
else
{
return for64Bits ? _levels64BitSparseGiant : _levels32BitSparseGiant;
}
return for64Bits ? _monoSparse64Bit : _monoSparse32Bit;
}
else
{
@@ -5,7 +5,7 @@ using ARMeilleure.IntermediateRepresentation;
using ARMeilleure.State;
using ARMeilleure.Translation;
using ARMeilleure.Translation.PTC;
using System.Linq;
using static ARMeilleure.Instructions.InstEmitHelper;
using static ARMeilleure.IntermediateRepresentation.Operand.Factory;
@@ -238,7 +238,7 @@ namespace ARMeilleure.Instructions
}
else if (table.Sparse)
{
// Inline table lookup. Only enabled when the sparse function table is enabled with 2 levels.
// Inline table lookup. Only enabled when the sparse function table is enabled with 1 level.
// Deliberately attempts to avoid branches.
Operand tableBase = !context.HasPtc ?
@@ -247,18 +247,15 @@ namespace ARMeilleure.Instructions
hostAddress = tableBase;
for (int i = 0; i < table.Levels.Length; i++)
{
AddressTableLevel level = table.Levels[i];
int clearBits = 64 - (level.Index + level.Length);
AddressTableLevel level = table.Levels.Last();
int clearBits = 64 - (level.Index + level.Length);
Operand index = context.ShiftLeft(
context.ShiftRightUI(context.ShiftLeft(guestAddress, Const(clearBits)), Const(clearBits + level.Index)),
Const(3)
);
Operand index = context.ShiftLeft(
context.ShiftRightUI(context.ShiftLeft(guestAddress, Const(clearBits)), Const(clearBits + level.Index)),
Const(3)
);
hostAddress = context.Load(OperandType.I64, context.Add(hostAddress, index));
}
hostAddress = context.Load(OperandType.I64, context.Add(hostAddress, index));
}
else
{
+1 -1
View File
@@ -2,7 +2,7 @@ using System;
namespace ARMeilleure.Memory
{
public class ReservedRegion
public class ReservedRegion : IDisposable
{
public const int DefaultGranularity = 65536; // Mapping granularity in Windows.
+7 -6
View File
@@ -1,5 +1,6 @@
using ARMeilleure.IntermediateRepresentation;
using ARMeilleure.Translation;
using ARMeilleure.Translation.Cache;
using System.Runtime.InteropServices;
using static ARMeilleure.IntermediateRepresentation.Operand.Factory;
@@ -17,7 +18,7 @@ namespace ARMeilleure.Signal
public delegate int DebugThreadLocalMapGetOrReserve(int threadId, int initialState);
public delegate void DebugNativeWriteLoop(nint nativeWriteLoopPtr, nint writePtr);
public static DebugPartialUnmap GenerateDebugPartialUnmap()
public static DebugPartialUnmap GenerateDebugPartialUnmap(JitCache jitCache)
{
EmitterContext context = new();
@@ -31,10 +32,10 @@ namespace ARMeilleure.Signal
OperandType[] argTypes = [OperandType.I64];
return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<DebugPartialUnmap>();
return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<DebugPartialUnmap>(jitCache);
}
public static DebugThreadLocalMapGetOrReserve GenerateDebugThreadLocalMapGetOrReserve(nint structPtr)
public static DebugThreadLocalMapGetOrReserve GenerateDebugThreadLocalMapGetOrReserve(JitCache jitCache, nint structPtr)
{
EmitterContext context = new();
@@ -48,10 +49,10 @@ namespace ARMeilleure.Signal
OperandType[] argTypes = [OperandType.I64];
return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<DebugThreadLocalMapGetOrReserve>();
return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<DebugThreadLocalMapGetOrReserve>(jitCache);
}
public static DebugNativeWriteLoop GenerateDebugNativeWriteLoop()
public static DebugNativeWriteLoop GenerateDebugNativeWriteLoop(JitCache jitCache)
{
EmitterContext context = new();
@@ -77,7 +78,7 @@ namespace ARMeilleure.Signal
OperandType[] argTypes = [OperandType.I64];
return Compiler.Compile(cfg, argTypes, OperandType.None, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<DebugNativeWriteLoop>();
return Compiler.Compile(cfg, argTypes, OperandType.None, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<DebugNativeWriteLoop>(jitCache);
}
}
}
@@ -4,7 +4,7 @@ using System.Diagnostics.CodeAnalysis;
namespace ARMeilleure.Translation.Cache
{
readonly struct CacheEntry : IComparable<CacheEntry>
public readonly struct CacheEntry : IComparable<CacheEntry>
{
public int Offset { get; }
public int Size { get; }
@@ -22,12 +22,31 @@ namespace ARMeilleure.Translation.Cache
return Offset.CompareTo(other.Offset);
}
}
private readonly int _regionSize;
private int _regionCount;
private readonly List<MemoryBlock> _blocks = [];
public CacheMemoryAllocator(int capacity)
public CacheMemoryAllocator(int regionSize, int initialRegionCount = 1)
{
_blocks.Add(new MemoryBlock(0, capacity));
_regionCount = 0;
_regionSize = regionSize;
for (; initialRegionCount > 0; initialRegionCount--)
{
_blocks.Add(new MemoryBlock(_regionSize * _regionCount, _regionSize));
_regionCount++;
}
}
public void AddNewBlocks(int count)
{
for (; count > 0; count--)
{
_blocks.Add(new MemoryBlock(_regionSize * _regionCount, _regionSize));
_regionCount++;
}
}
public int Allocate(int size)
@@ -65,13 +84,14 @@ namespace ARMeilleure.Translation.Cache
{
index = ~index;
}
int endOffs = block.Offset + block.Size;
if (index < _blocks.Count)
// Don't merge blocks from different allocations
if (index < _blocks.Count && endOffs % _regionSize != 0)
{
MemoryBlock next = _blocks[index];
int endOffs = block.Offset + block.Size;
if (next.Offset == endOffs)
{
block = new MemoryBlock(block.Offset, block.Size + next.Size);
@@ -79,7 +99,8 @@ namespace ARMeilleure.Translation.Cache
}
}
if (index > 0)
// Don't merge blocks from different allocations
if (index > 0 && block.Offset % _regionSize != 0)
{
MemoryBlock prev = _blocks[index - 1];
+84 -91
View File
@@ -14,62 +14,35 @@ using System.Threading;
namespace ARMeilleure.Translation.Cache
{
static partial class JitCache
public partial class JitCache : IDisposable
{
private static readonly int _pageSize = (int)MemoryBlock.GetPageSize();
private static readonly int _pageMask = _pageSize - 1;
private const int CodeAlignment = 4; // Bytes.
private const int CacheSize = 256 * 1024 * 1024;
private const uint CacheSize = 256 * 1024 * 1024;
private static JitCacheInvalidation _jitCacheInvalidator;
private readonly JitCacheInvalidation _jitCacheInvalidator;
private static readonly List<CacheMemoryAllocator> _cacheAllocators = [];
private readonly CacheMemoryAllocator _cacheAllocator;
private static readonly List<CacheEntry> _cacheEntries = [];
private readonly List<CacheEntry> _cacheEntries = [];
private static readonly Lock _lock = new();
private static bool _initialized;
private readonly Lock _lock = new();
private static readonly List<ReservedRegion> _jitRegions = [];
private static int _activeRegionIndex = 0;
private readonly List<ReservedRegion> _jitRegions = [];
[SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)]
public static partial nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize);
private static partial nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize);
public static void Initialize(IJitMemoryAllocator allocator)
public JitCache(IJitMemoryAllocator allocator)
{
lock (_lock)
{
if (_initialized)
{
if (OperatingSystem.IsWindows())
{
JitUnwindWindows.RemoveFunctionTableHandler(
_jitRegions[0].Pointer);
}
_jitRegions.Add(new(allocator, CacheSize));
for (int i = 0; i < _jitRegions.Count; i++)
{
_jitRegions[i].Dispose();
}
_jitRegions.Clear();
_cacheAllocators.Clear();
}
else
{
_initialized = true;
}
_activeRegionIndex = 0;
ReservedRegion firstRegion = new(allocator, CacheSize);
_jitRegions.Add(firstRegion);
CacheMemoryAllocator firstCacheAllocator = new(CacheSize);
_cacheAllocators.Add(firstCacheAllocator);
_cacheAllocator = new((int)CacheSize);
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
{
@@ -79,23 +52,20 @@ namespace ARMeilleure.Translation.Cache
if (OperatingSystem.IsWindows())
{
JitUnwindWindows.InstallFunctionTableHandler(
firstRegion.Pointer, CacheSize, firstRegion.Pointer + Allocate(_pageSize)
this, _jitRegions[0].Pointer, CacheSize, _jitRegions[0].Pointer + Allocate(_pageSize)
);
}
}
}
public static nint Map(CompiledFunction func)
public nint Map(CompiledFunction func)
{
byte[] code = func.Code;
lock (_lock)
{
Debug.Assert(_initialized);
int funcOffset = Allocate(code.Length);
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
nint funcPtr = targetRegion.Pointer + funcOffset;
nint funcPtr = GetFunctionPtr(funcOffset);
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
@@ -109,9 +79,9 @@ namespace ARMeilleure.Translation.Cache
}
else
{
ReprotectAsWritable(targetRegion, funcOffset, code.Length);
ReprotectAsWritable(funcOffset, code.Length);
Marshal.Copy(code, 0, funcPtr, code.Length);
ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
ReprotectAsExecutable(funcOffset, code.Length);
if (OperatingSystem.IsWindows() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
@@ -129,25 +99,24 @@ namespace ARMeilleure.Translation.Cache
}
}
public static void Unmap(nint pointer)
public void Unmap(nint pointer)
{
lock (_lock)
{
Debug.Assert(_initialized);
foreach (ReservedRegion region in _jitRegions)
for (int i = 0; i < _jitRegions.Count; i++)
{
if (pointer.ToInt64() < region.Pointer.ToInt64() ||
pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
ReservedRegion jitRegion = _jitRegions[i];
if (pointer.ToInt64() < jitRegion.Pointer.ToInt64() ||
pointer.ToInt64() >= (jitRegion.Pointer + (nint)CacheSize).ToInt64())
{
continue;
}
int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64());
int funcOffset = (int)(pointer.ToInt64() - jitRegion.Pointer.ToInt64() + i * CacheSize);
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{
_cacheAllocators[_activeRegionIndex].Free(funcOffset, AlignCodeSize(entry.Size));
_cacheAllocator.Free(funcOffset, AlignCodeSize(entry.Size));
_cacheEntries.RemoveAt(entryIndex);
}
@@ -156,53 +125,67 @@ namespace ARMeilleure.Translation.Cache
}
}
private static void ReprotectAsWritable(ReservedRegion region, int offset, int size)
private void ReprotectAsWritable(int offset, int size)
{
int endOffs = offset + size;
int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
int regionStart = (offset % (int)CacheSize) & ~_pageMask;
int regionEnd = endOffs % (int)CacheSize == 0
? (((int)CacheSize) + _pageMask) & ~_pageMask
: ((endOffs % (int)CacheSize) + _pageMask) & ~_pageMask;
region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
GetRegion(offset).Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
}
private static void ReprotectAsExecutable(ReservedRegion region, int offset, int size)
private void ReprotectAsExecutable(int offset, int size)
{
int endOffs = offset + size;
int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
int regionStart = (offset % (int)CacheSize) & ~_pageMask;
int regionEnd = endOffs % (int)CacheSize == 0
? (((int)CacheSize) + _pageMask) & ~_pageMask
: ((endOffs % (int)CacheSize) + _pageMask) & ~_pageMask;
region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
GetRegion(offset).Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
}
private static int Allocate(int codeSize)
private int Allocate(int codeSize)
{
codeSize = AlignCodeSize(codeSize);
int allocOffset = _cacheAllocators[_activeRegionIndex].Allocate(codeSize);
int allocOffset = _cacheAllocator.Allocate(codeSize);
if (allocOffset >= 0)
{
_jitRegions[_activeRegionIndex].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
GetRegion(allocOffset).ExpandIfNeeded((ulong)(allocOffset % (int)CacheSize) + (ulong)codeSize);
return allocOffset;
}
int exhaustedRegion = _activeRegionIndex;
_cacheAllocator.AddNewBlocks(1);
ReservedRegion newRegion = new(_jitRegions[0].Allocator, CacheSize);
Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache of size {(_jitRegions.Count * CacheSize).Bytes()} exhausted, creating new Cache Region ({((_jitRegions.Count + 1) * CacheSize).Bytes()} Total Allocation).");
_jitRegions.Add(newRegion);
_activeRegionIndex = _jitRegions.Count - 1;
Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {_activeRegionIndex} ({((long)(_activeRegionIndex + 1) * CacheSize).Bytes()} Total Allocation).");
_cacheAllocators.Add(new CacheMemoryAllocator(CacheSize));
int allocOffsetNew = _cacheAllocators[_activeRegionIndex].Allocate(codeSize);
if (allocOffsetNew < 0)
allocOffset = _cacheAllocator.Allocate(codeSize);
if (allocOffset < 0)
{
throw new OutOfMemoryException("Failed to allocate in new Cache Region!");
}
newRegion.ExpandIfNeeded((ulong)allocOffsetNew + (ulong)codeSize);
return allocOffsetNew;
GetRegion(allocOffset).ExpandIfNeeded((ulong)(allocOffset % (int)CacheSize) + (ulong)codeSize);
return allocOffset;
}
private nint GetFunctionPtr(int offset)
{
return GetRegion(offset).Pointer + (offset % (int)CacheSize);
}
private ReservedRegion GetRegion(int offset)
{
int index = offset / (int)CacheSize;
return _jitRegions[index];
}
private static int AlignCodeSize(int codeSize)
@@ -210,7 +193,7 @@ namespace ARMeilleure.Translation.Cache
return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1);
}
private static void Add(int offset, int size, UnwindInfo unwindInfo)
private void Add(int offset, int size, UnwindInfo unwindInfo)
{
CacheEntry entry = new(offset, size, unwindInfo);
@@ -224,25 +207,22 @@ namespace ARMeilleure.Translation.Cache
_cacheEntries.Insert(index, entry);
}
public static bool TryFind(int offset, out CacheEntry entry, out int entryIndex)
public bool TryFind(int offset, out CacheEntry entry, out int entryIndex)
{
lock (_lock)
{
foreach (ReservedRegion _ in _jitRegions)
int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default));
if (index < 0)
{
int index = _cacheEntries.BinarySearch(new CacheEntry(offset, 0, default));
index = ~index - 1;
}
if (index < 0)
{
index = ~index - 1;
}
if (index >= 0)
{
entry = _cacheEntries[index];
entryIndex = index;
return true;
}
if (index >= 0)
{
entry = _cacheEntries[index];
entryIndex = index;
return true;
}
}
@@ -250,5 +230,18 @@ namespace ARMeilleure.Translation.Cache
entryIndex = 0;
return false;
}
public void Dispose()
{
if (OperatingSystem.IsWindows())
{
JitUnwindWindows.RemoveFunctionTableHandler(_jitRegions[0].Pointer);
}
foreach (ReservedRegion jitRegion in _jitRegions)
{
jitRegion.Dispose();
}
}
}
}
@@ -2,6 +2,8 @@
using ARMeilleure.CodeGen.Unwinding;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
@@ -26,6 +28,29 @@ namespace ARMeilleure.Translation.Cache
public byte FrameRegister;
public unsafe fixed ushort UnwindCodes[MaxUnwindCodesArraySize];
}
private unsafe struct InternalFunctionHandler
{
public InternalFunctionHandler(JitCache jitCache, nint workBufferPtr)
{
_jitCache = jitCache;
_runtimeFunction = (RuntimeFunction*)workBufferPtr;
_unwindInfo = (UnwindInfo*)(workBufferPtr + _sizeOfRuntimeFunction);
}
readonly JitCache _jitCache;
readonly RuntimeFunction* _runtimeFunction;
readonly UnwindInfo* _unwindInfo;
public RuntimeFunction* FunctionTableHandler(ulong controlPc, nint context)
{
return JitUnwindWindows.FunctionTableHandler(_jitCache, _runtimeFunction, _unwindInfo, controlPc, context);
}
}
private enum UnwindOp
{
@@ -59,27 +84,28 @@ namespace ARMeilleure.Translation.Cache
private static GetRuntimeFunctionCallback _getRuntimeFunctionCallback;
private static int _sizeOfRuntimeFunction;
private static readonly int _sizeOfRuntimeFunction;
private static readonly ConcurrentDictionary<ulong, InternalFunctionHandler> _functionTableHandlers = new();
private unsafe static RuntimeFunction* _runtimeFunction;
static JitUnwindWindows()
{
_sizeOfRuntimeFunction = Marshal.SizeOf<RuntimeFunction>();
}
private unsafe static UnwindInfo* _unwindInfo;
public static void InstallFunctionTableHandler(nint codeCachePointer, uint codeCacheLength, nint workBufferPtr)
public static void InstallFunctionTableHandler(JitCache jitCache, nint codeCachePointer, uint codeCacheLength, nint workBufferPtr)
{
ulong codeCachePtr = (ulong)codeCachePointer.ToInt64();
_sizeOfRuntimeFunction = Marshal.SizeOf<RuntimeFunction>();
bool result;
InternalFunctionHandler handler;
unsafe
{
_runtimeFunction = (RuntimeFunction*)workBufferPtr;
handler = new InternalFunctionHandler(jitCache, workBufferPtr);
_unwindInfo = (UnwindInfo*)(workBufferPtr + _sizeOfRuntimeFunction);
_getRuntimeFunctionCallback = new GetRuntimeFunctionCallback(FunctionTableHandler);
_getRuntimeFunctionCallback = handler.FunctionTableHandler;
result = RtlInstallFunctionTableCallback(
codeCachePtr | 3,
@@ -94,6 +120,8 @@ namespace ARMeilleure.Translation.Cache
{
throw new InvalidOperationException("Failure installing function table callback.");
}
_functionTableHandlers.TryAdd(codeCachePtr, handler);
}
public static void RemoveFunctionTableHandler(nint codeCachePointer)
@@ -111,24 +139,26 @@ namespace ARMeilleure.Translation.Cache
{
throw new InvalidOperationException("Failure removing function table callback.");
}
_functionTableHandlers.Remove(codeCachePtr, out _);
}
private static unsafe RuntimeFunction* FunctionTableHandler(ulong controlPc, nint context)
private static unsafe RuntimeFunction* FunctionTableHandler(JitCache jitCache, RuntimeFunction* runtimeFunction, UnwindInfo* unwindInfo, ulong controlPc, nint context)
{
int offset = (int)((long)controlPc - context.ToInt64());
if (!JitCache.TryFind(offset, out CacheEntry funcEntry, out _))
if (!jitCache.TryFind(offset, out CacheEntry funcEntry, out _))
{
return null; // Not found.
}
CodeGen.Unwinding.UnwindInfo unwindInfo = funcEntry.UnwindInfo;
CodeGen.Unwinding.UnwindInfo funcUnwindInfo = funcEntry.UnwindInfo;
int codeIndex = 0;
for (int index = unwindInfo.PushEntries.Length - 1; index >= 0; index--)
for (int index = funcUnwindInfo.PushEntries.Length - 1; index >= 0; index--)
{
UnwindPushEntry entry = unwindInfo.PushEntries[index];
UnwindPushEntry entry = funcUnwindInfo.PushEntries[index];
switch (entry.PseudoOp)
{
@@ -140,14 +170,14 @@ namespace ARMeilleure.Translation.Cache
if (stackOffset <= 0xFFFF0)
{
_unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.SaveXmm128, entry.PrologOffset, entry.RegIndex);
_unwindInfo->UnwindCodes[codeIndex++] = (ushort)(stackOffset / 16);
unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.SaveXmm128, entry.PrologOffset, entry.RegIndex);
unwindInfo->UnwindCodes[codeIndex++] = (ushort)(stackOffset / 16);
}
else
{
_unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.SaveXmm128Far, entry.PrologOffset, entry.RegIndex);
_unwindInfo->UnwindCodes[codeIndex++] = (ushort)(stackOffset >> 0);
_unwindInfo->UnwindCodes[codeIndex++] = (ushort)(stackOffset >> 16);
unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.SaveXmm128Far, entry.PrologOffset, entry.RegIndex);
unwindInfo->UnwindCodes[codeIndex++] = (ushort)(stackOffset >> 0);
unwindInfo->UnwindCodes[codeIndex++] = (ushort)(stackOffset >> 16);
}
break;
@@ -161,18 +191,18 @@ namespace ARMeilleure.Translation.Cache
if (allocSize <= 128)
{
_unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.AllocSmall, entry.PrologOffset, (allocSize / 8) - 1);
unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.AllocSmall, entry.PrologOffset, (allocSize / 8) - 1);
}
else if (allocSize <= 0x7FFF8)
{
_unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.AllocLarge, entry.PrologOffset, 0);
_unwindInfo->UnwindCodes[codeIndex++] = (ushort)(allocSize / 8);
unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.AllocLarge, entry.PrologOffset, 0);
unwindInfo->UnwindCodes[codeIndex++] = (ushort)(allocSize / 8);
}
else
{
_unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.AllocLarge, entry.PrologOffset, 1);
_unwindInfo->UnwindCodes[codeIndex++] = (ushort)(allocSize >> 0);
_unwindInfo->UnwindCodes[codeIndex++] = (ushort)(allocSize >> 16);
unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.AllocLarge, entry.PrologOffset, 1);
unwindInfo->UnwindCodes[codeIndex++] = (ushort)(allocSize >> 0);
unwindInfo->UnwindCodes[codeIndex++] = (ushort)(allocSize >> 16);
}
break;
@@ -180,7 +210,7 @@ namespace ARMeilleure.Translation.Cache
case UnwindPseudoOp.PushReg:
{
_unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.PushNonvol, entry.PrologOffset, entry.RegIndex);
unwindInfo->UnwindCodes[codeIndex++] = PackUnwindOp(UnwindOp.PushNonvol, entry.PrologOffset, entry.RegIndex);
break;
}
@@ -192,16 +222,16 @@ namespace ARMeilleure.Translation.Cache
Debug.Assert(codeIndex <= MaxUnwindCodesArraySize);
_unwindInfo->VersionAndFlags = 1; // Flags: The function has no handler.
_unwindInfo->SizeOfProlog = (byte)unwindInfo.PrologSize;
_unwindInfo->CountOfUnwindCodes = (byte)codeIndex;
_unwindInfo->FrameRegister = 0;
unwindInfo->VersionAndFlags = 1; // Flags: The function has no handler.
unwindInfo->SizeOfProlog = (byte)funcUnwindInfo.PrologSize;
unwindInfo->CountOfUnwindCodes = (byte)codeIndex;
unwindInfo->FrameRegister = 0;
_runtimeFunction->BeginAddress = (uint)funcEntry.Offset;
_runtimeFunction->EndAddress = (uint)(funcEntry.Offset + funcEntry.Size);
_runtimeFunction->UnwindData = (uint)_sizeOfRuntimeFunction;
runtimeFunction->BeginAddress = (uint)funcEntry.Offset;
runtimeFunction->EndAddress = (uint)(funcEntry.Offset + funcEntry.Size);
runtimeFunction->UnwindData = (uint)_sizeOfRuntimeFunction;
return _runtimeFunction;
return runtimeFunction;
}
private static ushort PackUnwindOp(UnwindOp op, int prologOffset, int opInfo)
+9 -4
View File
@@ -4,6 +4,7 @@ using ARMeilleure.CodeGen.Unwinding;
using ARMeilleure.Common;
using ARMeilleure.Memory;
using ARMeilleure.State;
using ARMeilleure.Translation.Cache;
using Humanizer;
using Ryujinx.Common;
using Ryujinx.Common.Configuration;
@@ -33,7 +34,7 @@ namespace ARMeilleure.Translation.PTC
private const string OuterHeaderMagicString = "PTCohd\0\0";
private const string InnerHeaderMagicString = "PTCihd\0\0";
private const uint InternalVersion = 7010; //! To be incremented manually for each change to the ARMeilleure project.
private const uint InternalVersion = 7018; //! To be incremented manually for each change to the ARMeilleure project.
private const string ActualDir = "0";
private const string BackupDir = "1";
@@ -50,6 +51,8 @@ namespace ARMeilleure.Translation.PTC
private const CompressionLevel SaveCompressionLevel = CompressionLevel.Fastest;
public PtcProfiler Profiler { get; }
private readonly JitCache _jitCache;
// Carriers.
private MemoryStream _infosStream;
@@ -81,7 +84,7 @@ namespace ARMeilleure.Translation.PTC
private volatile int _translateTotalCount;
public event Action<PtcLoadingState, int, int> PtcStateChanged;
public Ptc()
public Ptc(JitCache jitCache)
{
Profiler = new PtcProfiler(this);
@@ -92,6 +95,8 @@ namespace ARMeilleure.Translation.PTC
_waitEvent = new ManualResetEvent(true);
_jitCache = jitCache;
_disposed = false;
TitleIdText = TitleIdTextDefault;
@@ -782,7 +787,7 @@ namespace ARMeilleure.Translation.PTC
return new UnwindInfo(pushEntries, prologueSize);
}
private static TranslatedFunction FastTranslate(
private TranslatedFunction FastTranslate(
byte[] code,
Counter<uint> callCounter,
ulong guestSize,
@@ -790,7 +795,7 @@ namespace ARMeilleure.Translation.PTC
bool highCq)
{
CompiledFunction cFunc = new(code, unwindInfo, RelocInfo.Empty);
GuestFunction gFunc = cFunc.MapWithPointer<GuestFunction>(out nint gFuncPointer);
GuestFunction gFunc = cFunc.MapWithPointer<GuestFunction>(_jitCache, out nint gFuncPointer);
return new TranslatedFunction(gFunc, gFuncPointer, callCounter, guestSize, highCq);
}
+7 -4
View File
@@ -24,6 +24,7 @@ namespace ARMeilleure.Translation
private readonly IJitMemoryAllocator _allocator;
private readonly ConcurrentQueue<KeyValuePair<ulong, TranslatedFunction>> _oldFuncs;
public readonly JitCache JitCache;
private readonly Ptc _ptc;
internal TranslatorCache<TranslatedFunction> Functions { get; }
@@ -43,16 +44,17 @@ namespace ARMeilleure.Translation
_oldFuncs = new ConcurrentQueue<KeyValuePair<ulong, TranslatedFunction>>();
_ptc = new Ptc();
JitCache = new JitCache(allocator);
_ptc = new Ptc(JitCache);
Queue = new TranslatorQueue();
JitCache.Initialize(allocator);
CountTable = new EntryTable<uint>();
Functions = new TranslatorCache<TranslatedFunction>();
FunctionTable = functionTable;
Stubs = new TranslatorStubs(FunctionTable);
Stubs = new TranslatorStubs(JitCache, FunctionTable);
FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub;
}
@@ -167,6 +169,7 @@ namespace ARMeilleure.Translation
}
ClearJitCache();
JitCache.Dispose();
Stubs.Dispose();
FunctionTable.Dispose();
@@ -305,7 +308,7 @@ namespace ARMeilleure.Translation
_ptc.WriteCompiledFunction(address, funcSize, hash, highCq, compiledFunc);
}
GuestFunction func = compiledFunc.MapWithPointer<GuestFunction>(out nint funcPointer);
GuestFunction func = compiledFunc.MapWithPointer<GuestFunction>(JitCache, out nint funcPointer);
Allocators.ResetAll();
+13 -8
View File
@@ -14,6 +14,8 @@ namespace ARMeilleure.Translation
/// </summary>
class TranslatorStubs : IDisposable
{
private readonly JitCache _jitCache;
private readonly Lazy<nint> _slowDispatchStub;
private bool _disposed;
@@ -83,11 +85,14 @@ namespace ARMeilleure.Translation
/// Initializes a new instance of the <see cref="TranslatorStubs"/> class with the specified
/// <see cref="Translator"/> instance.
/// </summary>
/// <param name="jitCache">Jit cache to map functions in</param>
/// <param name="functionTable">Function table used to store pointers to the functions that the guest code will call</param>
/// <exception cref="ArgumentNullException"><paramref name="translator"/> is null</exception>
public TranslatorStubs(IAddressTable<ulong> functionTable)
public TranslatorStubs(JitCache jitCache, IAddressTable<ulong> functionTable)
{
ArgumentNullException.ThrowIfNull(functionTable);
_jitCache = jitCache;
_functionTable = functionTable;
_slowDispatchStub = new(GenerateSlowDispatchStub, isThreadSafe: true);
@@ -115,12 +120,12 @@ namespace ARMeilleure.Translation
{
if (_dispatchStub.IsValueCreated)
{
JitCache.Unmap(_dispatchStub.Value);
_jitCache.Unmap(_dispatchStub.Value);
}
if (_dispatchLoop.IsValueCreated)
{
JitCache.Unmap(Marshal.GetFunctionPointerForDelegate(_dispatchLoop.Value));
_jitCache.Unmap(Marshal.GetFunctionPointerForDelegate(_dispatchLoop.Value));
}
_disposed = true;
@@ -156,7 +161,7 @@ namespace ARMeilleure.Translation
context.BranchIfTrue(lblFallback, masked);
Operand index = default;
Operand page = Const((long)_functionTable.Base);
Operand page = Const(_functionTable.Base);
for (int i = 0; i < _functionTable.Levels.Length; i++)
{
@@ -188,7 +193,7 @@ namespace ARMeilleure.Translation
OperandType retType = OperandType.I64;
OperandType[] argTypes = [OperandType.I64];
GuestFunction func = Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<GuestFunction>();
GuestFunction func = Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<GuestFunction>(_jitCache);
return Marshal.GetFunctionPointerForDelegate(func);
}
@@ -213,7 +218,7 @@ namespace ARMeilleure.Translation
OperandType retType = OperandType.I64;
OperandType[] argTypes = [OperandType.I64];
GuestFunction func = Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<GuestFunction>();
GuestFunction func = Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<GuestFunction>(_jitCache);
return Marshal.GetFunctionPointerForDelegate(func);
}
@@ -290,7 +295,7 @@ namespace ARMeilleure.Translation
OperandType retType = OperandType.None;
OperandType[] argTypes = [OperandType.I64, OperandType.I64];
return Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<DispatcherFunction>();
return Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<DispatcherFunction>(_jitCache);
}
/// <summary>
@@ -314,7 +319,7 @@ namespace ARMeilleure.Translation
OperandType retType = OperandType.I64;
OperandType[] argTypes = [OperandType.I64, OperandType.I64];
return Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<WrapperFunction>();
return Compiler.Compile(cfg, argTypes, retType, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<WrapperFunction>(_jitCache);
}
}
}
@@ -1,6 +1,7 @@
using ARMeilleure.CodeGen.X86;
using ARMeilleure.IntermediateRepresentation;
using ARMeilleure.State;
using ARMeilleure.Translation.Cache;
using System;
using System.Runtime.InteropServices;
using static ARMeilleure.IntermediateRepresentation.Operand.Factory;
@@ -59,7 +60,7 @@ namespace ARMeilleure.Translation
return context.VectorExtract(OperandType.I32, vec, 0);
}
public static FpFlagsPInvokeTest GenerateFpFlagsPInvokeTest()
public static FpFlagsPInvokeTest GenerateFpFlagsPInvokeTest(JitCache jitCache)
{
EmitterContext context = new();
@@ -141,7 +142,7 @@ namespace ARMeilleure.Translation
OperandType[] argTypes = [OperandType.I64];
return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<FpFlagsPInvokeTest>();
return Compiler.Compile(cfg, argTypes, OperandType.I32, CompilerOptions.HighCq, RuntimeInformation.ProcessArchitecture).Map<FpFlagsPInvokeTest>(jitCache);
}
}
}
@@ -128,7 +128,7 @@ namespace Ryujinx.BuildValidationTasks
using (StreamWriter sw = new(path))
{
sw.Write(jsonString);
sw.Write($"{jsonString}\n");
}
}
+9 -7
View File
@@ -17,6 +17,7 @@ namespace Ryujinx.Common
//Dragon Quest Franchise
"010008900705c000", // Dragon Quest Builders
"010042000a986000", // Dragon Quest Builders 2
"010021701c45e000", // Dragon Quest VII Reimagined Demo
//Fire Emblem Franchise
"0100a6301214e000", // Fire Emblem Engage
@@ -33,7 +34,6 @@ namespace Ryujinx.Common
"01007e3006dda000", // Kirby Star Allies
"01003fb00c5a8000", // Super Kirby Clash
//The Zelda Franchise
"01000b900d8b0000", // Cadence of Hyrule
"0100ae00096ea000", // Hyrule Warriors: Definitive Edition
@@ -43,8 +43,7 @@ namespace Ryujinx.Common
"01002da013484000", // The Legend of Zelda: Skyward Sword HD
"0100f2c0115b6000", // The Legend of Zelda: Tears of the Kingdom
"01008cf01baac000", // The Legend of Zelda: Echoes of Wisdom
//Luigi Franchise
"010048701995e000", // Luigi's Mansion 2 HD
"0100dca0064a6000", // Luigi's Mansion 3
@@ -75,16 +74,13 @@ namespace Ryujinx.Common
"010049900f546002", // Super Mario 3D All-Stars | Super Mario Sunshine
"010049900f546003", // Super Mario 3D All-Stars | Super Mario Galaxy
"010028600ebda000", // Super Mario 3D World + Bowser's Fury
"010049900F546001", // Super Mario 64
"0100ea80032ea000", // Super Mario Bros. U Deluxe
"010015100b514000", // Super Mario Bros. Wonder
"010049900F546003", // Super Mario Galaxy
"01009b90006dc000", // Super Mario Maker 2
"0100000000010000", // SUPER MARIO ODYSSEY
"010036b0034e4000", // Super Mario Party
"0100965017338000", // Super Mario Party Jamboree
"0100bc0018138000", // Super Mario RPG
"010049900F546002", // Super Mario Sunshine
"0100a3900c3e2000", // Paper Mario: The Origami King
"0100ecd018ebe000", // Paper Mario: The Thousand-Year Door
"01009cC022b96000", // Super Mario Galaxy
@@ -149,6 +145,7 @@ namespace Ryujinx.Common
"010067b017588000", // Endless Ocean Luminous
"01003da010e8a000", // Miitopia
"01006b5012b32000", // Part Time UFO
"0100107027310000", // Rhythm Heave Groove: Starter Demo
"0100704000B3A000", // Snipperclips
"01006a800016e000", // Super Smash Bros. Ultimate
"0100a9400c9c2000", // Tokyo Mirage Sessions #FE Encore
@@ -176,7 +173,7 @@ namespace Ryujinx.Common
"01004ad014bf0000", // Sonic Frontiers
"01009aa000faa000", // Sonic Mania
"01005ea01c0fc000", // SONIC X SHADOW GENERATIONS
"01005ea01c0fc001", // ^
"01005ea01c0fc001", // ^ TODO: (Needs to be differentiated like 3D All Stars)
"01006e001823c000", // Sonic Racing: CrossWorlds
"010064b0242be000", // Sonic Racing: CrossWorlds - Demo
@@ -199,6 +196,7 @@ namespace Ryujinx.Common
"0100744001588000", // Cars 3: Driven to Win
"0100b41013c82000", // Cruis'n Blast
"010085900337e000", // Death Squared
"0100a0d022a68000", // DELTARUNE
"01001b300b9be000", // Diablo III: Eternal Collection
"010027400cdc6000", // Divinity Original 2 - Definitive Edition
"01008c8012920000", // Dying Light Platinum Edition
@@ -211,6 +209,8 @@ namespace Ryujinx.Common
"010013c00e930000", // Hollow Knight: Silksong
"010085500130a000", // Lego City: Undercover
"010073c01af34000", // LEGO Horizon Adventures
"0100653026dde000", // MARVEL Cosmic Invasion Demo
"0100419014456000", // Mary Skelter Finale
"0100d71004694000", // Minecraft
"01007430037f6000", // Monopoly
"0100853015e86000", // No Man's Sky
@@ -231,6 +231,8 @@ namespace Ryujinx.Common
"01008d100d43e000", // Saints Row IV
"0100de600beee000", // Saints Row: The Third - The Full Package
"01001180021fa000", // Shovel Knight: Specter of Torment
"0100ccc0002e6000", // Skylanders Imaginators (US)
"010048d000bfc000", // Skylanders Imaginators (EU)
"010079f00671c000", // Sparkle 2: Evo
"010077b00e046000", // Spyro: Reignited Trilogy
"0100e1D01eb2e000", // Squeakross: Home Squeak Home
+46 -26
View File
@@ -81,12 +81,13 @@ namespace ARMeilleure.Common
private bool _disposed;
private TEntry** _table;
private TEntry* _sparseTable;
private readonly List<AddressTablePage> _pages;
private TEntry _fill;
private readonly MemoryBlock _sparseFill;
private readonly SparseMemoryBlock _fillBottomLevel;
private readonly TEntry* _fillBottomLevelPtr;
private MemoryBlock _sparseFill;
private SparseMemoryBlock _fillBottomLevel;
private TEntry* _fillBottomLevelPtr;
private readonly List<TableSparseBlock> _sparseReserved;
private readonly ReaderWriterLockSlim _sparseLock;
@@ -122,16 +123,23 @@ namespace ARMeilleure.Common
{
ObjectDisposedException.ThrowIf(_disposed, this);
lock (_pages)
if (Sparse)
{
return (nint)GetRootPage();
return (nint)_sparseTable;
}
else
{
lock (_pages)
{
return (nint)GetRootPage();
}
}
}
}
/// <summary>
/// Constructs a new instance of the <see cref="AddressTable{TEntry}"/> class with the specified list of
/// <see cref="Level"/>.
/// <see cref="AddressTableLevel"/>.
/// </summary>
/// <param name="levels">Levels for the address table</param>
/// <param name="sparse">True if the bottom page should be sparsely mapped</param>
@@ -156,18 +164,8 @@ namespace ARMeilleure.Common
if (sparse)
{
// If the address table is sparse, allocate a fill block
_sparseFill = new MemoryBlock(268435456ul, MemoryAllocationFlags.Mirrorable); //low Power TC uses size: 65536ul
ulong bottomLevelSize = (1ul << levels.Last().Length) * (ulong)sizeof(TEntry);
_fillBottomLevel = new SparseMemoryBlock(bottomLevelSize, null, _sparseFill);
_fillBottomLevelPtr = (TEntry*)_fillBottomLevel.Block.Pointer;
_sparseReserved = [];
_sparseLock = new ReaderWriterLockSlim();
_sparseBlockSize = bottomLevelSize;
}
}
@@ -209,13 +207,24 @@ namespace ARMeilleure.Common
public void SignalCodeRange(ulong address, ulong size)
{
AddressTableLevel bottom = Levels.Last();
ulong bottomLevelEntries = 1ul << bottom.Length;
ulong entryIndex = address >> bottom.Index;
ulong entries = size >> bottom.Index;
entries += entryIndex - BitUtils.AlignDown(entryIndex, bottomLevelEntries);
if (Sparse)
{
ulong bottomLevelSize = (ulong)BitUtils.Pow2RoundUp((int)entries) * (ulong)sizeof(TEntry);
_sparseFill = new MemoryBlock(bottomLevelSize, MemoryAllocationFlags.Mirrorable);
_sparseBlockSize = Math.Max(_sparseBlockSize, BitUtils.AlignUp(entries, bottomLevelEntries) * (ulong)sizeof(TEntry));
_fillBottomLevel = new SparseMemoryBlock(bottomLevelSize, null, _sparseFill);
_fillBottomLevelPtr = (TEntry*)_fillBottomLevel.Block.Pointer;
_sparseBlockSize = bottomLevelSize;
_sparseTable = (TEntry*)Allocate((int)entries, Fill, leaf: true);
_sparseTable -= Levels.Last().GetValue(address);
}
}
/// <inheritdoc/>
@@ -234,15 +243,26 @@ namespace ARMeilleure.Common
throw new ArgumentException($"Address 0x{address:X} is not mapped onto the table.", nameof(address));
}
lock (_pages)
if (Sparse)
{
TEntry* page = GetPage(address);
long index = Levels.Last().GetValue(address);
EnsureMapped((nint)(_sparseTable + index));
return ref _sparseTable[index];
}
else
{
lock (_pages)
{
TEntry* page = GetPage(address);
long index = Levels[^1].GetValue(address);
long index = Levels.Last().GetValue(address);
EnsureMapped((nint)(page + index));
EnsureMapped((nint)(page + index));
return ref page[index];
return ref page[index];
}
}
}
@@ -3,6 +3,7 @@ using Ryujinx.Cpu.LightningJit.CodeGen;
using Ryujinx.Cpu.LightningJit.CodeGen.Arm64;
using System;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
@@ -189,7 +190,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64
}
else if (inlineLookup)
{
// Inline table lookup. Only enabled when the sparse function table is enabled with 2 levels.
// Inline table lookup. Only enabled when the sparse function table is enabled with 1 level.
Operand indexReg = Register(NextFreeRegister(tempRegister + 1, tempGuestAddress));
@@ -203,18 +204,15 @@ namespace Ryujinx.Cpu.LightningJit.Arm32.Target.Arm64
// Index into the table.
asm.Mov(rn, tableBase);
for (int i = 0; i < funcTable.Levels.Length; i++)
{
AddressTableLevel level = funcTable.Levels[i];
asm.Ubfx(indexReg, guestAddress, level.Index, level.Length);
asm.Lsl(indexReg, indexReg, Const(3));
AddressTableLevel level = funcTable.Levels.Last();
asm.Ubfx(indexReg, guestAddress, level.Index, level.Length);
asm.Lsl(indexReg, indexReg, Const(3));
// Index into the page.
asm.Add(rn, rn, indexReg);
// Index into the page.
asm.Add(rn, rn, indexReg);
// Load the page address.
asm.LdrRiUn(rn, rn, 0);
}
// Load the page address.
asm.LdrRiUn(rn, rn, 0);
if (tempGuestAddress != -1)
{
@@ -3,6 +3,7 @@ using Ryujinx.Cpu.LightningJit.CodeGen;
using Ryujinx.Cpu.LightningJit.CodeGen.Arm64;
using System;
using System.Diagnostics;
using System.Linq;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@@ -355,7 +356,7 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
}
else if (inlineLookup)
{
// Inline table lookup. Only enabled when the sparse function table is enabled with 2 levels.
// Inline table lookup. Only enabled when the sparse function table is enabled with 1 level.
Operand indexReg = Register(NextFreeRegister(tempRegister + 1, tempGuestAddress));
@@ -369,18 +370,15 @@ namespace Ryujinx.Cpu.LightningJit.Arm64.Target.Arm64
// Index into the table.
asm.Mov(rn, tableBase);
for (int i = 0; i < funcTable.Levels.Length; i++)
{
AddressTableLevel level = funcTable.Levels[i];
asm.Ubfx(indexReg, guestAddress, level.Index, level.Length);
asm.Lsl(indexReg, indexReg, Const(3));
AddressTableLevel level = funcTable.Levels.Last();
asm.Ubfx(indexReg, guestAddress, level.Index, level.Length);
asm.Lsl(indexReg, indexReg, Const(3));
// Index into the page.
asm.Add(rn, rn, indexReg);
// Index into the page.
asm.Add(rn, rn, indexReg);
// Load the page address.
asm.LdrRiUn(rn, rn, 0);
}
// Load the page address.
asm.LdrRiUn(rn, rn, 0);
if (tempGuestAddress != -1)
{
@@ -23,12 +23,31 @@ namespace Ryujinx.Cpu.LightningJit.Cache
return Offset.CompareTo(other.Offset);
}
}
private readonly int _regionSize;
private int _regionCount;
private readonly List<MemoryBlock> _blocks = [];
public CacheMemoryAllocator(int capacity)
public CacheMemoryAllocator(int regionSize, int initialRegionCount = 1)
{
_blocks.Add(new MemoryBlock(0, capacity));
_regionCount = 0;
_regionSize = regionSize;
for (; initialRegionCount > 0; initialRegionCount--)
{
_blocks.Add(new MemoryBlock(_regionSize * _regionCount, _regionSize));
_regionCount++;
}
}
public void AddNewBlocks(int count)
{
for (; count > 0; count--)
{
_blocks.Add(new MemoryBlock(_regionSize * _regionCount, _regionSize));
_regionCount++;
}
}
public int Allocate(int size)
@@ -100,13 +119,14 @@ namespace Ryujinx.Cpu.LightningJit.Cache
{
index = ~index;
}
int endOffs = block.Offset + block.Size;
if (index < _blocks.Count)
// Don't merge blocks from different allocations
if (index < _blocks.Count && endOffs % _regionSize != 0)
{
MemoryBlock next = _blocks[index];
int endOffs = block.Offset + block.Size;
if (next.Offset == endOffs)
{
block = new MemoryBlock(block.Offset, block.Size + next.Size);
@@ -114,7 +134,8 @@ namespace Ryujinx.Cpu.LightningJit.Cache
}
}
if (index > 0)
// Don't merge blocks from different allocations
if (index > 0 && block.Offset % _regionSize != 0)
{
MemoryBlock prev = _blocks[index - 1];
+66 -86
View File
@@ -11,76 +11,45 @@ using System.Threading;
namespace Ryujinx.Cpu.LightningJit.Cache
{
static partial class JitCache
partial class JitCache : IDisposable
{
private static readonly int _pageSize = (int)MemoryBlock.GetPageSize();
private static readonly int _pageMask = _pageSize - 1;
private const int CodeAlignment = 4; // Bytes.
// TODO: JIT Cache size should be application dependent, not global.
private const int CacheSize = 1024 * (1024 * 1024); // Megabytes * Size of Megabytes (since its in bytes).
private const uint CacheSize = 256 * 1024 * 1024; // Megabytes * Size of Megabytes (since its in bytes).
private static JitCacheInvalidation _jitCacheInvalidator;
private readonly JitCacheInvalidation _jitCacheInvalidator;
private static CacheMemoryAllocator _cacheAllocator;
private readonly CacheMemoryAllocator _cacheAllocator;
private static readonly List<CacheEntry> _cacheEntries = [];
private readonly List<CacheEntry> _cacheEntries = [];
private static readonly Lock _lock = new();
private static bool _initialized;
private static readonly List<ReservedRegion> _jitRegions = [];
private static int _activeRegionIndex = 0;
[SupportedOSPlatform("windows")]
[LibraryImport("kernel32.dll", SetLastError = true)]
public static partial nint FlushInstructionCache(nint hProcess, nint lpAddress, nuint dwSize);
private readonly Lock _lock = new();
[SupportedOSPlatform("macos")]
[LibraryImport("libSystem.dylib", EntryPoint = "sys_icache_invalidate")]
internal static partial void SysICacheInvalidate(nint start, nuint len);
private readonly List<ReservedRegion> _jitRegions = [];
[SupportedOSPlatform("linux")]
[LibraryImport("libgcc_s.so.1", EntryPoint = "__clear_cache")]
internal static partial void ClearCache(nint begin, nint end);
public static void Initialize(IJitMemoryAllocator allocator)
public JitCache(IJitMemoryAllocator allocator)
{
if (_initialized)
{
return;
}
lock (_lock)
{
if (_initialized)
{
return;
}
ReservedRegion firstRegion = new(allocator, CacheSize);
_jitRegions.Add(firstRegion);
_activeRegionIndex = 0;
_jitRegions.Add(new(allocator, CacheSize));
_cacheAllocator = new CacheMemoryAllocator((int)CacheSize);
if (!OperatingSystem.IsWindows() && !OperatingSystem.IsMacOS())
{
_jitCacheInvalidator = new JitCacheInvalidation(allocator);
}
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
_initialized = true;
}
}
public unsafe static nint Map(ReadOnlySpan<byte> code)
public nint Map(ReadOnlySpan<byte> code)
{
lock (_lock)
{
Debug.Assert(_initialized);
int funcOffset = Allocate(code.Length);
ReservedRegion targetRegion = _jitRegions[_activeRegionIndex];
nint funcPtr = targetRegion.Pointer + funcOffset;
nint funcPtr = GetFunctionPtr(funcOffset);
if (OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64)
{
@@ -94,9 +63,9 @@ namespace Ryujinx.Cpu.LightningJit.Cache
}
else
{
ReprotectAsWritable(targetRegion, funcOffset, code.Length);
ReprotectAsWritable(funcOffset, code.Length);
Marshal.Copy(code.ToArray(), 0, funcPtr, code.Length);
ReprotectAsExecutable(targetRegion, funcOffset, code.Length);
ReprotectAsExecutable(funcOffset, code.Length);
_jitCacheInvalidator?.Invalidate(funcPtr, (ulong)code.Length);
}
@@ -107,21 +76,20 @@ namespace Ryujinx.Cpu.LightningJit.Cache
}
}
public static void Unmap(nint pointer)
public void Unmap(nint pointer)
{
lock (_lock)
{
Debug.Assert(_initialized);
foreach (ReservedRegion region in _jitRegions)
for (int i = 0; i < _jitRegions.Count; i++)
{
ReservedRegion region = _jitRegions[i];
if (pointer.ToInt64() < region.Pointer.ToInt64() ||
pointer.ToInt64() >= (region.Pointer + CacheSize).ToInt64())
pointer.ToInt64() >= (region.Pointer + (nint)CacheSize).ToInt64())
{
continue;
}
int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64());
int funcOffset = (int)(pointer.ToInt64() - region.Pointer.ToInt64() + i * CacheSize);
if (TryFind(funcOffset, out CacheEntry entry, out int entryIndex) && entry.Offset == funcOffset)
{
@@ -134,59 +102,63 @@ namespace Ryujinx.Cpu.LightningJit.Cache
}
}
private static void ReprotectAsWritable(ReservedRegion region, int offset, int size)
private void ReprotectAsWritable(int offset, int size)
{
int endOffs = offset + size;
int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
int regionStart = (offset % (int)CacheSize) & ~_pageMask;
int regionEnd = ((endOffs % (int)CacheSize) + _pageMask) & ~_pageMask;
region.Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
GetRegion(offset).Block.MapAsRwx((ulong)regionStart, (ulong)(regionEnd - regionStart));
}
private static void ReprotectAsExecutable(ReservedRegion region, int offset, int size)
private void ReprotectAsExecutable(int offset, int size)
{
int endOffs = offset + size;
int regionStart = offset & ~_pageMask;
int regionEnd = (endOffs + _pageMask) & ~_pageMask;
int regionStart = (offset % (int)CacheSize) & ~_pageMask;
int regionEnd = ((endOffs % (int)CacheSize) + _pageMask) & ~_pageMask;
region.Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
GetRegion(offset).Block.MapAsRx((ulong)regionStart, (ulong)(regionEnd - regionStart));
}
private static int Allocate(int codeSize)
private int Allocate(int codeSize)
{
codeSize = AlignCodeSize(codeSize);
int allocOffset = _cacheAllocator.Allocate(codeSize);
for (int i = _activeRegionIndex; i < _jitRegions.Count; i++)
if (allocOffset >= 0)
{
int allocOffset = _cacheAllocator.Allocate(codeSize);
if (allocOffset >= 0)
{
_jitRegions[i].ExpandIfNeeded((ulong)allocOffset + (ulong)codeSize);
_activeRegionIndex = i;
return allocOffset;
}
GetRegion(allocOffset).ExpandIfNeeded((ulong)(allocOffset % (int)CacheSize) + (ulong)codeSize);
return allocOffset;
}
int exhaustedRegion = _activeRegionIndex;
_cacheAllocator.AddNewBlocks(1);
ReservedRegion newRegion = new(_jitRegions[0].Allocator, CacheSize);
Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache of size {(_jitRegions.Count * CacheSize).Bytes()} exhausted, creating new Cache Region ({((_jitRegions.Count + 1) * CacheSize).Bytes()} Total Allocation).");
_jitRegions.Add(newRegion);
_activeRegionIndex = _jitRegions.Count - 1;
int newRegionNumber = _activeRegionIndex;
Logger.Warning?.Print(LogClass.Cpu, $"JIT Cache Region {exhaustedRegion} exhausted, creating new Cache Region {newRegionNumber} ({((long)(newRegionNumber + 1) * CacheSize).Bytes()} Total Allocation).");
_cacheAllocator = new CacheMemoryAllocator(CacheSize);
int allocOffsetNew = _cacheAllocator.Allocate(codeSize);
if (allocOffsetNew < 0)
allocOffset = _cacheAllocator.Allocate(codeSize);
if (allocOffset < 0)
{
throw new OutOfMemoryException("Failed to allocate in new Cache Region!");
}
newRegion.ExpandIfNeeded((ulong)allocOffsetNew + (ulong)codeSize);
return allocOffsetNew;
GetRegion(allocOffset).ExpandIfNeeded((ulong)(allocOffset % (int)CacheSize) + (ulong)codeSize);
return allocOffset;
}
private nint GetFunctionPtr(int offset)
{
return GetRegion(offset).Pointer + (offset % (int)CacheSize);
}
private ReservedRegion GetRegion(int offset)
{
int index = offset / (int)CacheSize;
return _jitRegions[index];
}
private static int AlignCodeSize(int codeSize)
@@ -194,7 +166,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
return checked(codeSize + (CodeAlignment - 1)) & ~(CodeAlignment - 1);
}
private static void Add(int offset, int size)
private void Add(int offset, int size)
{
CacheEntry entry = new(offset, size);
@@ -208,7 +180,7 @@ namespace Ryujinx.Cpu.LightningJit.Cache
_cacheEntries.Insert(index, entry);
}
public static bool TryFind(int offset, out CacheEntry entry, out int entryIndex)
public bool TryFind(int offset, out CacheEntry entry, out int entryIndex)
{
lock (_lock)
{
@@ -231,5 +203,13 @@ namespace Ryujinx.Cpu.LightningJit.Cache
entryIndex = 0;
return false;
}
public void Dispose()
{
foreach (ReservedRegion jitRegion in _jitRegions)
{
jitRegion.Dispose();
}
}
}
}
+8 -6
View File
@@ -19,6 +19,7 @@ namespace Ryujinx.Cpu.LightningJit
private static bool IsNoWxPlatform => false;
private readonly ConcurrentQueue<KeyValuePair<ulong, TranslatedFunction>> _oldFuncs;
private readonly JitCache _jitCache;
private readonly NoWxCache _noWxCache;
private bool _disposed;
@@ -39,12 +40,12 @@ namespace Ryujinx.Cpu.LightningJit
}
else
{
JitCache.Initialize(new JitMemoryAllocator(forJit: true));
_jitCache = new(new JitMemoryAllocator(forJit: true));
}
Functions = new TranslatorCache<TranslatedFunction>();
FunctionTable = functionTable;
Stubs = new TranslatorStubs(FunctionTable, _noWxCache);
Stubs = new TranslatorStubs(_jitCache, FunctionTable, _noWxCache);
FunctionTable.Fill = (ulong)Stubs.SlowDispatchStub;
@@ -100,7 +101,7 @@ namespace Ryujinx.Cpu.LightningJit
if (oldFunc != func)
{
JitCache.Unmap(func.FuncPointer);
_jitCache.Unmap(func.FuncPointer);
func = oldFunc;
}
@@ -121,7 +122,7 @@ namespace Ryujinx.Cpu.LightningJit
private TranslatedFunction Translate(ulong address, ExecutionMode mode)
{
CompiledFunction func = Compile(address, mode);
nint funcPointer = JitCache.Map(func.Code);
nint funcPointer = _jitCache.Map(func.Code);
return new TranslatedFunction(funcPointer, (ulong)func.GuestCodeLength);
}
@@ -164,14 +165,14 @@ namespace Ryujinx.Cpu.LightningJit
foreach (TranslatedFunction func in functions)
{
JitCache.Unmap(func.FuncPointer);
_jitCache.Unmap(func.FuncPointer);
}
Functions.Clear();
while (_oldFuncs.TryDequeue(out KeyValuePair<ulong, TranslatedFunction> kv))
{
JitCache.Unmap(kv.Value.FuncPointer);
_jitCache.Unmap(kv.Value.FuncPointer);
}
}
@@ -188,6 +189,7 @@ namespace Ryujinx.Cpu.LightningJit
else
{
ClearJitCache();
_jitCache.Dispose();
}
Stubs.Dispose();
@@ -19,6 +19,8 @@ namespace Ryujinx.Cpu.LightningJit
{
private delegate ulong GetFunctionAddressDelegate(nint framePointer, ulong address);
private readonly JitCache _jitCache;
private readonly Lazy<nint> _slowDispatchStub;
private bool _disposed;
@@ -76,13 +78,16 @@ namespace Ryujinx.Cpu.LightningJit
/// Initializes a new instance of the <see cref="TranslatorStubs"/> class with the specified
/// <see cref="Translator"/> instance.
/// </summary>
/// <param name="jitCache">Jit cache to map functions in</param>
/// <param name="functionTable">Function table used to store pointers to the functions that the guest code will call</param>
/// <param name="noWxCache">Cache used on platforms that enforce W^X, otherwise should be null</param>
/// <exception cref="ArgumentNullException"><paramref name="translator"/> is null</exception>
public TranslatorStubs(IAddressTable<ulong> functionTable, NoWxCache noWxCache)
public TranslatorStubs(JitCache jitCache, IAddressTable<ulong> functionTable, NoWxCache noWxCache)
{
ArgumentNullException.ThrowIfNull(functionTable);
_jitCache = jitCache;
_functionTable = functionTable;
_noWxCache = noWxCache;
_getFunctionAddressRef = NativeInterface.GetFunctionAddress;
@@ -113,12 +118,12 @@ namespace Ryujinx.Cpu.LightningJit
{
if (_dispatchStub.IsValueCreated)
{
JitCache.Unmap(_dispatchStub.Value);
_jitCache.Unmap(_dispatchStub.Value);
}
if (_dispatchLoop.IsValueCreated)
{
JitCache.Unmap(Marshal.GetFunctionPointerForDelegate(_dispatchLoop.Value));
_jitCache.Unmap(Marshal.GetFunctionPointerForDelegate(_dispatchLoop.Value));
}
}
@@ -363,7 +368,7 @@ namespace Ryujinx.Cpu.LightningJit
}
else
{
return JitCache.Map(code);
return _jitCache.Map(code);
}
}
+52
View File
@@ -11,6 +11,9 @@ namespace Ryujinx.Graphics.Nvdec.Vp9
public bool IsHardwareAccelerated => false;
private readonly MemoryAllocator _allocator = new();
private byte[] _lastSegmentMap = [];
private int _lastSegmentMapMiRows;
private int _lastSegmentMapMiCols;
public ISurface CreateSurface(int width, int height)
{
@@ -106,7 +109,9 @@ namespace Ryujinx.Graphics.Nvdec.Vp9
cm.AllocTileWorkerData(_allocator, tileCols, tileRows, maxThreads);
cm.AllocContextBuffers(_allocator, output.Width, output.Height);
cm.InitContextBuffers();
LoadSegmentMap(ref cm, ref pictureInfo);
cm.SetupSegmentationDequant();
cm.SetupScaleFactors();
@@ -153,6 +158,7 @@ namespace Ryujinx.Graphics.Nvdec.Vp9
}
}
SaveSegmentMap(ref cm);
cm.GetMvs(mvsOut);
cm.FreeTileWorkerData(_allocator);
@@ -161,6 +167,52 @@ namespace Ryujinx.Graphics.Nvdec.Vp9
return true;
}
private void LoadSegmentMap(ref Vp9Common cm, ref Vp9PictureInfo pictureInfo)
{
int mapLength = cm.MiRows * cm.MiCols;
if (mapLength == 0 || cm.LastFrameSegMap.IsNull)
{
return;
}
bool reset =
pictureInfo.IsKeyFrame ||
pictureInfo.IntraOnly ||
_lastSegmentMapMiRows != cm.MiRows ||
_lastSegmentMapMiCols != cm.MiCols;
if (_lastSegmentMap.Length != mapLength)
{
_lastSegmentMap = new byte[mapLength];
}
else if (reset)
{
Array.Clear(_lastSegmentMap);
}
_lastSegmentMapMiRows = cm.MiRows;
_lastSegmentMapMiCols = cm.MiCols;
_lastSegmentMap.CopyTo(cm.LastFrameSegMap.AsSpan());
}
private void SaveSegmentMap(ref Vp9Common cm)
{
int mapLength = cm.MiRows * cm.MiCols;
if (!cm.Seg.Enabled || mapLength == 0 || cm.CurrentFrameSegMap.IsNull)
{
return;
}
if (_lastSegmentMap.Length != mapLength)
{
_lastSegmentMap = new byte[mapLength];
}
cm.CurrentFrameSegMap.AsSpan()[..mapLength].CopyTo(_lastSegmentMap);
_lastSegmentMapMiRows = cm.MiRows;
_lastSegmentMapMiCols = cm.MiCols;
}
public void Dispose()
{
_allocator.Dispose();
@@ -1088,6 +1088,12 @@ namespace Ryujinx.Graphics.Nvdec.Vp9.Types
public void GetMvs(Span<Vp9MvRef> mvs)
{
if (FrameIsIntraOnly())
{
mvs.Clear();
return;
}
if (mvs.Length > CurFrameMvs.Length)
{
throw new ArgumentException(
@@ -53,6 +53,7 @@ namespace Ryujinx.Graphics.Nvdec.Types.Vp9
IsKeyFrame = Flags.HasFlag(FrameFlags.IsKeyFrame),
IntraOnly = Flags.HasFlag(FrameFlags.IntraOnly),
UsePrevInFindMvRefs =
!Flags.HasFlag(FrameFlags.IsKeyFrame) &&
!Flags.HasFlag(FrameFlags.ErrorResilientMode) &&
!Flags.HasFlag(FrameFlags.FrameSizeChanged) &&
!Flags.HasFlag(FrameFlags.IntraOnly) &&
@@ -11,7 +11,11 @@ namespace Ryujinx.HLE.HOS.Services.Notification
private readonly KEvent _getNotificationSendingNotifierEvent;
private int _getNotificationSendingNotifierEventHandle;
public INotificationSystemEventAccessor(ServiceCtx context) { }
public INotificationSystemEventAccessor(ServiceCtx context)
{
_getNotificationSendingNotifierEvent = new KEvent(context.Device.System.KernelContext);
}
[CommandCmif(0)] // 9.0.0+
// GetNotificationSendingNotifier() -> nn::notification::server::INotificationSystemEventAccessor
@@ -103,8 +103,12 @@ namespace Ryujinx.HLE.HOS.Services.Sdb.Pl
loadedCount++;
}
// The first return value is a boolean "fonts_are_loaded" flag (u8), not a count.
// Some titles (e.g. newer SDK builds) validate it strictly against 1, so writing
// the font count here (e.g. 6) makes them reject the result and spin re-opening pl:u.
context.ResponseData.Write(1);
context.ResponseData.Write(loadedCount);
context.ResponseData.Write((int)SharedFontType.Count);
// context.ResponseData.Write((int)SharedFontType.Count);
return ResultCode.Success;
}
+97 -27
View File
@@ -16,10 +16,9 @@ namespace Ryujinx.Horizon.MmNv.Ipc
public Result InitializeOld(Module module, uint fgmPriority, uint autoClearEvent)
{
bool isAutoClearEvent = autoClearEvent != 0;
uint requestId = Register(module, fgmPriority, isAutoClearEvent);
Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module, fgmPriority, isAutoClearEvent });
Register(module, fgmPriority, isAutoClearEvent);
Logger.Info?.PrintMsg(LogClass.ServiceMm, $"Initialized module={module} requestId={requestId} autoClearEvent={isAutoClearEvent}");
return Result.Success;
}
@@ -27,41 +26,48 @@ namespace Ryujinx.Horizon.MmNv.Ipc
[CmifCommand(1)]
public Result FinalizeOld(Module module)
{
Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module });
lock (_sessionList)
{
_sessionList.Remove(GetSessionByModule(module));
Session session = GetSessionByModule(module);
if (session != null)
{
_sessionList.Remove(session);
}
}
Logger.Info?.PrintMsg(LogClass.ServiceMm, $"Finalized module={module}");
return Result.Success;
}
[CmifCommand(2)]
public Result SetAndWaitOld(Module module, uint clockRateMin, int clockRateMax)
public Result SetAndWaitOld(Module module, uint clockRateMin, uint timeout)
{
Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module, clockRateMin, clockRateMax });
uint actualClockRate;
lock (_sessionList)
{
GetSessionByModule(module)?.SetAndWait(clockRateMin, clockRateMax);
Session session = GetSessionByModule(module) ?? RegisterSession(module, false);
ApplyClockRequest(session, clockRateMin, timeout);
actualClockRate = GetEffectiveClockRate(module);
}
Logger.Trace?.PrintMsg(LogClass.ServiceMm, $"SetAndWait module={module} requested={clockRateMin} actual={actualClockRate} timeout={timeout}");
return Result.Success;
}
[CmifCommand(3)]
public Result GetOld(out uint clockRateActual, Module module)
{
Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module });
lock (_sessionList)
{
Session session = GetSessionByModule(module);
clockRateActual = session == null ? 0 : session.ClockRateMin;
clockRateActual = GetEffectiveClockRate(module);
}
Logger.Trace?.PrintMsg(LogClass.ServiceMm, $"Get module={module} actual={clockRateActual}");
return Result.Success;
}
@@ -70,51 +76,76 @@ namespace Ryujinx.Horizon.MmNv.Ipc
{
bool isAutoClearEvent = autoClearEvent != 0;
Logger.Stub?.PrintStub(LogClass.ServiceMm, new { module, fgmPriority, isAutoClearEvent });
requestId = Register(module, fgmPriority, isAutoClearEvent);
Logger.Info?.PrintMsg(LogClass.ServiceMm, $"Initialized module={module} requestId={requestId} autoClearEvent={isAutoClearEvent}");
return Result.Success;
}
[CmifCommand(5)]
public Result Finalize(uint requestId)
{
Logger.Stub?.PrintStub(LogClass.ServiceMm, new { requestId });
lock (_sessionList)
{
_sessionList.Remove(GetSessionById(requestId));
Session session = GetSessionById(requestId);
if (session != null)
{
_sessionList.Remove(session);
}
}
Logger.Info?.PrintMsg(LogClass.ServiceMm, $"Finalized requestId={requestId}");
return Result.Success;
}
[CmifCommand(6)]
public Result SetAndWait(uint requestId, uint clockRateMin, int clockRateMax)
public Result SetAndWait(uint requestId, uint clockRateMin, uint timeout)
{
Logger.Stub?.PrintStub(LogClass.ServiceMm, new { requestId, clockRateMin, clockRateMax });
uint actualClockRate = 0;
Module? module = null;
lock (_sessionList)
{
GetSessionById(requestId)?.SetAndWait(clockRateMin, clockRateMax);
Session session = GetSessionById(requestId);
if (session != null)
{
module = session.Module;
ApplyClockRequest(session, clockRateMin, timeout);
actualClockRate = GetEffectiveClockRate(session.Module);
}
}
Logger.Trace?.PrintMsg(LogClass.ServiceMm, $"SetAndWait requestId={requestId} module={module?.ToString() ?? "<missing>"} requested={clockRateMin} actual={actualClockRate} timeout={timeout}");
return Result.Success;
}
[CmifCommand(7)]
public Result Get(out uint clockRateActual, uint requestId)
{
Logger.Stub?.PrintStub(LogClass.ServiceMm, new { requestId });
Module? module = null;
lock (_sessionList)
{
Session session = GetSessionById(requestId);
clockRateActual = session == null ? 0 : session.ClockRateMin;
if (session == null)
{
clockRateActual = 0;
}
else
{
module = session.Module;
clockRateActual = GetEffectiveClockRate(session.Module);
}
}
Logger.Trace?.PrintMsg(LogClass.ServiceMm, $"Get requestId={requestId} module={module?.ToString() ?? "<missing>"} actual={clockRateActual}");
return Result.Success;
}
@@ -149,12 +180,51 @@ namespace Ryujinx.Horizon.MmNv.Ipc
lock (_sessionList)
{
// Nintendo ignores the fgm priority as the other services were deprecated.
Session session = new(_uniqueId++, module, isAutoClearEvent);
_sessionList.Add(session);
Session session = RegisterSession(module, isAutoClearEvent);
return session.Id;
}
}
private Session RegisterSession(Module module, bool isAutoClearEvent)
{
Session session = new(_uniqueId++, module, isAutoClearEvent);
_sessionList.Add(session);
return session;
}
private void ApplyClockRequest(Session session, uint requestedClockRate, uint timeout)
{
if (IsClockRateSentinel(requestedClockRate))
{
session.ClearRequest(timeout);
}
else
{
session.SetAndWait(requestedClockRate, timeout);
}
}
private uint GetEffectiveClockRate(Module module)
{
uint clockRate = 0;
foreach (Session session in _sessionList)
{
if (session.Module == module && session.HasActiveRequest && session.RequestedClockRate > clockRate)
{
clockRate = session.RequestedClockRate;
}
}
return clockRate;
}
private static bool IsClockRateSentinel(uint clockRate)
{
return clockRate == 0 || unchecked((int)clockRate) < 0;
}
}
}
+2 -2
View File
@@ -7,11 +7,11 @@ namespace Ryujinx.Horizon.Sdk.MmNv
{
Result InitializeOld(Module module, uint fgmPriority, uint autoClearEvent);
Result FinalizeOld(Module module);
Result SetAndWaitOld(Module module, uint clockRateMin, int clockRateMax);
Result SetAndWaitOld(Module module, uint clockRateMin, uint timeout);
Result GetOld(out uint clockRateActual, Module module);
Result Initialize(out uint requestId, Module module, uint fgmPriority, uint autoClearEvent);
Result Finalize(uint requestId);
Result SetAndWait(uint requestId, uint clockRateMin, int clockRateMax);
Result SetAndWait(uint requestId, uint clockRateMin, uint timeout);
Result Get(out uint clockRateActual, uint requestId);
}
}
+17 -7
View File
@@ -5,22 +5,32 @@ namespace Ryujinx.Horizon.Sdk.MmNv
public Module Module { get; }
public uint Id { get; }
public bool IsAutoClearEvent { get; }
public uint ClockRateMin { get; private set; }
public int ClockRateMax { get; private set; }
public uint RequestedClockRate { get; private set; }
public uint LastTimeout { get; private set; }
public bool HasActiveRequest { get; private set; }
public Session(uint id, Module module, bool isAutoClearEvent)
{
Module = module;
Id = id;
IsAutoClearEvent = isAutoClearEvent;
ClockRateMin = 0;
ClockRateMax = -1;
RequestedClockRate = 0;
LastTimeout = uint.MaxValue;
HasActiveRequest = false;
}
public void SetAndWait(uint clockRateMin, int clockRateMax)
public void SetAndWait(uint clockRate, uint timeout)
{
ClockRateMin = clockRateMin;
ClockRateMax = clockRateMax;
RequestedClockRate = clockRate;
LastTimeout = timeout;
HasActiveRequest = true;
}
public void ClearRequest(uint timeout)
{
RequestedClockRate = 0;
LastTimeout = timeout;
HasActiveRequest = false;
}
}
}
+1 -1
View File
@@ -41,7 +41,7 @@ namespace Ryujinx.Input.SDL3
}
int serialNumber = 0;
string? serial = SDL_GetGamepadSerial(gamepadHandle);
string serial = SDL_GetGamepadSerial(gamepadHandle);
if (serial is not null)
{
int.TryParse(serial, out serialNumber);
+10 -8
View File
@@ -701,16 +701,17 @@ namespace Ryujinx.Input.HLE
VibrationValue leftVibrationValue = dualVibrationValue.Item1;
VibrationValue rightVibrationValue = dualVibrationValue.Item2;
float low = Math.Min(1f, (float)((rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15) * controllerConfig.Rumble.StrongRumble));
float high = Math.Min(1f, (float)((leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85) * controllerConfig.Rumble.WeakRumble));
leftVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
leftVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
rightVibrationValue.AmplitudeLow *= controllerConfig.Rumble.WeakRumble;
rightVibrationValue.AmplitudeHigh *= controllerConfig.Rumble.StrongRumble;
if (!controllerConfig.Rumble.UseHDRumble || gamepad.HDRumble(leftVibrationValue, rightVibrationValue) == false)
if (!controllerConfig.Rumble.UseHDRumble || !gamepad.HDRumble(leftVibrationValue, rightVibrationValue))
{
float low = Math.Min(1f, (float)(rightVibrationValue.AmplitudeLow * 0.85 + rightVibrationValue.AmplitudeHigh * 0.15));
float high = Math.Min(1f, (float)(leftVibrationValue.AmplitudeLow * 0.15 + leftVibrationValue.AmplitudeHigh * 0.85));
gamepad.Rumble(low, high, 0xFFFFFFFF);
}
@@ -767,8 +768,9 @@ namespace Ryujinx.Input.HLE
{
foreach (AssignedInputDevice assignedController in assignedControllers)
{
foreach (string gamepadId in gamepadDriver.GamepadsIds)
for (int index = 0; index < gamepadDriver.GamepadsIds.Length; index++)
{
string gamepadId = gamepadDriver.GamepadsIds[index];
if (string.Equals(gamepadId, assignedController.Id, StringComparison.Ordinal))
{
yield return assignedController;
@@ -782,14 +784,14 @@ namespace Ryujinx.Input.HLE
if (config is StandardControllerInputConfig)
{
foreach (string gamepadId in gamepadDriver.GamepadsIds)
for (int index = 0; index < gamepadDriver.GamepadsIds.Length; index++)
{
string gamepadId = gamepadDriver.GamepadsIds[index];
if (string.Equals(gamepadId, config.Id, StringComparison.Ordinal))
{
yield return new AssignedInputDevice
{
Type = AssignedInputDeviceType.Controller,
Id = gamepadId,
Type = AssignedInputDeviceType.Controller, Id = gamepadId,
};
yield break;
}
@@ -1092,7 +1094,7 @@ namespace Ryujinx.Input.HLE
: _assignedControllerConfigs.FirstOrDefault(),
_controllerDriver
),
_ => ((IGamepad?)null, (InputConfig?)null, (IGamepadDriver?)null)
_ => ((IGamepad)null, (InputConfig)null, (IGamepadDriver)null)
};
_controllerGamepad = _gamepad;
+2
View File
@@ -10,6 +10,8 @@ using MemoryPermission = Ryujinx.Tests.Unicorn.MemoryPermission;
namespace Ryujinx.Tests.Cpu
{
[TestFixture]
[Parallelizable(ParallelScope.All)]
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
public class CpuTest
{
protected static readonly ulong Size = MemoryBlock.GetPageSize();
+2
View File
@@ -10,6 +10,8 @@ using MemoryPermission = Ryujinx.Tests.Unicorn.MemoryPermission;
namespace Ryujinx.Tests.Cpu
{
[TestFixture]
[Parallelizable(ParallelScope.All)]
[FixtureLifeCycle(LifeCycle.InstancePerTestCase)]
public class CpuTest32
{
protected static readonly uint Size = (uint)MemoryBlock.GetPageSize();
+1 -1
View File
@@ -51,7 +51,7 @@ namespace Ryujinx.Tests.Cpu
bool methodCalled = false;
bool isFz = false;
TranslatorTestMethods.FpFlagsPInvokeTest method = TranslatorTestMethods.GenerateFpFlagsPInvokeTest();
TranslatorTestMethods.FpFlagsPInvokeTest method = TranslatorTestMethods.GenerateFpFlagsPInvokeTest(_translator.JitCache);
// This method sets flush-to-zero and then calls the managed method.
// Before and after setting the flags, it ensures subnormal addition works as expected.
+2 -2
View File
@@ -245,7 +245,7 @@ namespace Ryujinx.Tests.Memory
// Create a large mapping.
mainMemory.MapView(backing, 0, 0, vaSize);
TestMethods.DebugNativeWriteLoop writeFunc = TestMethods.GenerateDebugNativeWriteLoop();
TestMethods.DebugNativeWriteLoop writeFunc = TestMethods.GenerateDebugNativeWriteLoop(_translator.JitCache);
nint writePtr = mainMemory.GetPointer(vaSize - 0x1000, 4);
Thread testThread = new(() =>
@@ -339,7 +339,7 @@ namespace Ryujinx.Tests.Memory
fixed (void* localMap = &state.LocalCounts)
{
TestMethods.DebugThreadLocalMapGetOrReserve getOrReserve = TestMethods.GenerateDebugThreadLocalMapGetOrReserve((nint)localMap);
TestMethods.DebugThreadLocalMapGetOrReserve getOrReserve = TestMethods.GenerateDebugThreadLocalMapGetOrReserve(_translator.JitCache, (nint)localMap);
for (int i = 0; i < ThreadLocalMap<int>.MapSize; i++)
{
+31 -1
View File
@@ -1,7 +1,37 @@
{
"Locales": {
"ar_SA": [],
"de_DE": [],
"de_DE": [
"Ryubing ist mein zweiter Vorname.",
"Hier schlechten Witz einfügen.",
"ES IST RYUBING-ZEIT!",
"Fantastisch!",
"Jetzt mit 100% mehr Humor!",
"Jetzt geht es erst richtig los...",
"War wohl nur der Wind...",
"Kann Spuren von Nüssen enthalten!",
"Aus 100% biologischem Anbau!",
"Schönen Tag noch! : )",
"Der Weltraum... unendliche Weiten!",
"Falls du es noch nicht bemerkt hast: Ich liebe es schlechte Witze zu machen : )",
"In C# programmiert!",
"Forgejo hat Gitlab entthront!",
"Irgendwelche Ideen, was hier stehen könnte?",
"Guten Morgen!",
"Guten Tag!",
"Guten Abend!",
"Ich hoffe, du hast einen tollen Tag!",
"ICH... BIN RYUBING!",
"Nein, Lossless Scaling wird NICHT unterstützt.",
"Trans-Rechte sind Menschenrechte!",
":3",
"Bitte einen Controller anschließen!",
"Never gonna give you up!",
"Ganon beobachtet dich!",
"Unzählige Stunden voller Spaß!",
"Yahaha! Du hast mich endeckt!",
"Finde uns auf BlueSky"
],
"el_GR": [],
"en_US": [
"Ryubing is my middle name.",
+28 -28
View File
@@ -2,7 +2,6 @@ using System.Collections.Generic;
using Ryujinx.Common.Logging;
using Gommon;
using Ryujinx.Ava.Systems.Configuration;
using System;
using System.Text.Json;
namespace Ryujinx.Common
@@ -11,47 +10,48 @@ namespace Ryujinx.Common
{
public static void PrintSplash()
{
string splash = GetSplash();
Logger.Notice.Print(LogClass.Application, " ___ __ _ ");
Logger.Notice.Print(LogClass.Application, @" / _ \ __ __ __ __ / / (_) ___ ___ _");
Logger.Notice.Print(LogClass.Application, @" / , _/ / // // // / / _ \ / / / _ \ / _ `/");
Logger.Notice.Print(LogClass.Application, @"/_/|_| \_, / \_,_/ /_.__//_/ /_//_/ \_, / ");
Logger.Notice.Print(LogClass.Application, " /___/ /___/ ");
Logger.Notice.Print(LogClass.Application, "");
Logger.Notice.Print(LogClass.Application, GetSplash());
Logger.Notice.Print(LogClass.Application, "");
if (splash is null)
{
Logger.Error?.Print(LogClass.Application, "Failed to fetch Splash Text! Splash JSON is invalid!");
return;
}
if (!splash.IsNullOrEmpty())
{
Logger.Notice.Print(LogClass.Application, "");
Logger.Notice.Print(LogClass.Application, splash);
Logger.Notice.Print(LogClass.Application, "");
}
}
private static string s_finalSplash = "";
private static string _finalSplash;
public static string GetSplash()
{
if (string.IsNullOrEmpty(s_finalSplash))
if (_finalSplash is null)
{
s_finalSplash = GetLangJson();
if (string.IsNullOrEmpty(s_finalSplash))
try
{
s_finalSplash = "Splash Text";
string data;
data = EmbeddedResources.ReadAllText("Ryujinx/Assets/Splashes.json");
SplashLocales splashJson = JsonSerializer.Deserialize<SplashLocales>(data);
_finalSplash = splashJson.Locales[ConfigurationState.Instance.UI.LanguageCode.Value].GetRandomElement() ?? "";
}
catch
{
return null;
}
}
return $"{s_finalSplash}";
}
private static SplashLocales s_splashJson;
private static string GetLangJson()
{
try
{
string data;
data = EmbeddedResources.ReadAllText("Ryujinx/Assets/Splashes.json");
s_splashJson = JsonSerializer.Deserialize<SplashLocales>(data);
return s_splashJson.Locales[ConfigurationState.Instance.UI.LanguageCode.Value].GetRandomElement();
}
catch
{
return "";
}
return _finalSplash;
}
private struct SplashLocales
+2 -2
View File
@@ -26,9 +26,9 @@ namespace Ryujinx.Ava.Common
internal class TrimmerWindow : Ryujinx.Common.Logging.XCIFileTrimmerLog
{
private readonly XCITrimmerViewModel _viewModel;
private readonly XciTrimmerViewModel _viewModel;
public TrimmerWindow(XCITrimmerViewModel viewModel)
public TrimmerWindow(XciTrimmerViewModel viewModel)
{
_viewModel = viewModel;
}
@@ -502,15 +502,20 @@ namespace Ryujinx.Ava.Systems.Configuration
public bool DebuggerSuspendOnStart { get; set; }
/// <summary>
/// Show toggles for dirty hacks in the UI.
/// Show toggles for dirty hacks in the UI.
/// </summary>
public bool ShowDirtyHacks { get; set; }
/// <summary>
/// The packed values of the enabled dirty hacks.
/// The packed values of the enabled dirty hacks.
/// </summary>
public ulong[] DirtyHacks { get; set; }
/// <summary>
/// Pause emulation while scanning Amiibo
/// </summary>
public bool PauseEmulationWhileScanningAmiibo { get; set; }
/// <summary>
/// Loads a configuration file from disk
/// </summary>
@@ -115,7 +115,7 @@ namespace Ryujinx.Ava.Systems.Configuration
System.SkipUserProfilesManager.Value = cff.SkipUserProfiles;
System.UseHypervisor.Value = cff.UseHypervisor;
System.GCLowLatency.Value = cff.GCLowLatency;
UI.GuiColumns.FavColumn.Value = shouldLoadFromFile ? cff.GuiColumns.FavColumn : UI.GuiColumns.FavColumn.Value;
UI.GuiColumns.IconColumn.Value = shouldLoadFromFile ? cff.GuiColumns.IconColumn : UI.GuiColumns.IconColumn.Value;
UI.GuiColumns.AppColumn.Value = shouldLoadFromFile ? cff.GuiColumns.AppColumn : UI.GuiColumns.AppColumn.Value;
@@ -151,6 +151,7 @@ namespace Ryujinx.Ava.Systems.Configuration
UI.WindowStartup.WindowPositionX.Value = shouldLoadFromFile ? cff.WindowStartup.WindowPositionX : UI.WindowStartup.WindowPositionX.Value;
UI.WindowStartup.WindowPositionY.Value = shouldLoadFromFile ? cff.WindowStartup.WindowPositionY : UI.WindowStartup.WindowPositionY.Value;
UI.WindowStartup.WindowMaximized.Value = shouldLoadFromFile ? cff.WindowStartup.WindowMaximized : UI.WindowStartup.WindowMaximized.Value;
UI.PauseEmulationWhileScanningAmiibo.Value = cff.PauseEmulationWhileScanningAmiibo;
Hid.EnableKeyboard.Value = cff.EnableKeyboard;
Hid.EnableMouse.Value = cff.EnableMouse;
@@ -196,6 +196,11 @@ namespace Ryujinx.Ava.Systems.Configuration
/// </summary>
public ReactiveObject<bool> ShowDynamicInputSwapWarning { get; private set; }
/// <summary>
/// Pause emulation while scanning Amiibo
/// </summary>
public ReactiveObject<bool> PauseEmulationWhileScanningAmiibo { get; private set; }
public UISection()
{
GuiColumns = new Columns();
@@ -217,6 +222,8 @@ namespace Ryujinx.Ava.Systems.Configuration
ShowConsole.Event += static (_, e) => ConsoleHelper.SetConsoleWindowState(e.NewValue);
ShowDynamicInputSwapWarning = new ReactiveObject<bool>();
ShowDynamicInputSwapWarning.Value = true;
PauseEmulationWhileScanningAmiibo = new ReactiveObject<bool>();
PauseEmulationWhileScanningAmiibo.Value = true;
}
}
@@ -158,6 +158,7 @@ namespace Ryujinx.Ava.Systems.Configuration
DebuggerSuspendOnStart = Debug.DebuggerSuspendOnStart,
ShowDirtyHacks = Hacks.ShowDirtyHacks,
DirtyHacks = Hacks.EnabledHacks.Select(it => it.Pack()).ToArray(),
PauseEmulationWhileScanningAmiibo = UI.PauseEmulationWhileScanningAmiibo,
};
return configurationFile;
@@ -262,6 +263,7 @@ namespace Ryujinx.Ava.Systems.Configuration
UI.WindowStartup.WindowPositionX.Value = 0;
UI.WindowStartup.WindowPositionY.Value = 0;
UI.WindowStartup.WindowMaximized.Value = false;
UI.PauseEmulationWhileScanningAmiibo.Value = true;
Hid.EnableKeyboard.Value = false;
Hid.EnableMouse.Value = false;
Hid.DisableInputWhenOutOfFocus.Value = false;
@@ -9,7 +9,9 @@ using Ryujinx.Common.Logging;
using Ryujinx.HLE;
using Ryujinx.HLE.Loaders.Processes;
using Ryujinx.Horizon;
using System.Collections.Generic;
using System.Text;
using System.Text.Json;
namespace Ryujinx.Ava.Systems
{
@@ -135,20 +137,48 @@ namespace Ryujinx.Ava.Systems
if (!formattedValue.Handled)
return;
_discordPresencePlaying.Details = TruncateToByteLength(
formattedValue.Reset
? $"Playing {_currentApp.Title}"
: formattedValue.FormattedString
);
try // New format that attempts to deserialize json, and if it fails (using old method)...
{
Dictionary<string, string> outDictionary = JsonSerializer.Deserialize<Dictionary<string, string>>(formattedValue.FormattedString);
_discordPresencePlaying.Details = TruncateToByteLength(
outDictionary["Details"].IsNullOrEmpty()
? $"Playing {_currentApp.Title}"
: outDictionary["Details"]
);
_discordPresencePlaying.State = TruncateToByteLength(
outDictionary["State"].IsNullOrEmpty()
? $"Total play time: {ValueFormatUtils.FormatTimeSpan(_currentApp.TimePlayed)}"
: outDictionary["State"]
);
if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details))
}
catch // Utilize original code
{
_discordPresencePlaying.Details = TruncateToByteLength(
formattedValue.Reset
? $"Playing {_currentApp.Title}"
: formattedValue.FormattedString
);
}
if (_discordClient.CurrentPresence.Details.Equals(_discordPresencePlaying.Details) && _discordClient.CurrentPresence.State.Equals(_discordPresencePlaying.State))
return; //don't trigger an update if the set presence Details are identical to current
_discordClient.SetPresence(_discordPresencePlaying);
Logger.Info?.Print(LogClass.UI, "Updated Discord RPC based on a supported play report.");
}
public static string PrepareMultilineRpcString(string line1 = "", string line2 = "")
{
Dictionary<string, string> rpcdict = new() { { "Details", line1 }, {"State", line2} };
return JsonSerializer.Serialize(rpcdict);
}
private static string TruncateToByteLength(string input)
{
if (Encoding.UTF8.GetByteCount(input) <= ApplicationByteLimit)
@@ -1105,7 +1105,7 @@ namespace Ryujinx.Ava.Systems.PlayReport
return $"At the main menu";
static string LocFinal(string? location) => location switch
static string LocFinal(string location) => location switch
{
"0" => "Somewhere in Miitopia",
"1" => "Wandering around Greenhorne",
@@ -1151,8 +1151,10 @@ namespace Ryujinx.Ava.Systems.PlayReport
{
return "At the main menu";
}
static string MarioOrLuigiGamemode(string? gamemode) => gamemode switch
return "";
static string MarioOrLuigiGamemode(string gamemode) => gamemode switch
{
"0" => "mario",
"1" => "luigi",
@@ -1161,14 +1163,14 @@ namespace Ryujinx.Ava.Systems.PlayReport
_ => gamemode
};
static string OtherGameMode(string? gamemode) => gamemode switch
{
"2" => "Boost Rush",
"3" => "Challenges",
"4" => "Coin Battle",
"5" => "Coin Battle Editor",
_ => ""
};
// static string OtherGameMode(string gamemode) => gamemode switch
// {
// "2" => "Boost Rush",
// "3" => "Challenges",
// "4" => "Coin Battle",
// "5" => "Coin Battle Editor",
// _ => ""
// };
static string SpecialMapNames(int? course) => course switch
{
@@ -1196,9 +1198,6 @@ namespace Ryujinx.Ava.Systems.PlayReport
// Glacier seals = 16, water leaf = 15
// desert ice = 14, acorn squid = 13
// all other course numbers are to be considered a hazard
return "";
}
}
}
+3 -3
View File
@@ -187,7 +187,7 @@ namespace Ryujinx.Ava.UI.Applet
StringLengthMax = 25
};
(UserResult result, string userInput) =
await SwkbdAppletDialog.ShowInputDialog(LocaleManager.Instance[LocaleKeys.CabinetDialog], args);
await SwkbdAppletDialog.ShowInputDialog(LocaleManager.Instance[LocaleKeys.Dialog_Amiibo_RenameAmiiboTitle], args);
if (result == UserResult.Ok)
{
inputText = userInput;
@@ -212,11 +212,11 @@ namespace Ryujinx.Ava.UI.Applet
{
dialogCloseEvent.Set();
await ContentDialogHelper.CreateInfoDialog(
LocaleManager.Instance[LocaleKeys.CabinetScanDialog],
LocaleManager.Instance[LocaleKeys.Dialog_Amiibo_ScanAmiiboMessage],
string.Empty,
LocaleManager.Instance[LocaleKeys.InputDialogOk],
string.Empty,
LocaleManager.Instance[LocaleKeys.CabinetTitle]
LocaleManager.Instance[LocaleKeys.Dialog_Amiibo_ScanAmiiboTitle]
);
});
dialogCloseEvent.WaitOne();
@@ -74,10 +74,14 @@ namespace Ryujinx.Ava.UI.Controls
content._host = contentDialog;
contentDialog.Title = title;
contentDialog.PrimaryButtonText = args.SubmitText;
contentDialog.PrimaryButtonText = string.IsNullOrWhiteSpace(args.SubmitText)
? LocaleManager.Instance[LocaleKeys.InputDialogOk]
: args.SubmitText;
contentDialog.IsPrimaryButtonEnabled = content._checkLength(content.Message.Length);
contentDialog.SecondaryButtonText = string.Empty;
contentDialog.CloseButtonText = LocaleManager.Instance[LocaleKeys.InputDialogCancel];
contentDialog.CloseButtonText = LocaleManager.Instance[LocaleKeys.Common_Buttons_Cancel];
contentDialog.Content = content;
void Handler(ContentDialog sender, ContentDialogClosedEventArgs eventArgs)
@@ -108,7 +108,7 @@ namespace Ryujinx.Ava.UI.Helpers
try
{
string labelsJson = File.ReadAllText(labelsPath);
Dictionary<string, string>? labels = JsonSerializer.Deserialize<Dictionary<string, string>>(labelsJson, _serializerOptions);
Dictionary<string, string> labels = JsonSerializer.Deserialize<Dictionary<string, string>>(labelsJson, _serializerOptions);
if (labels != null)
{
+177 -118
View File
@@ -3,8 +3,10 @@ using Avalonia.Collections;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using Path = System.IO.Path;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Common.Models.Amiibo;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.Helpers;
using Ryujinx.Ava.UI.Windows;
using Ryujinx.Ava.Utilities;
@@ -20,31 +22,39 @@ using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.ViewModels
{
public partial class AmiiboWindowViewModel : BaseModel, IDisposable
{
// ReSharper disable once InconsistentNaming
private static bool _cachedUseRandomUuid;
private const string DefaultJson = "{ \"amiibo\": [] }";
private const float AmiiboImageSize = 350f;
private readonly string _amiiboJsonPath;
private readonly byte[] _amiiboLogoBytes;
private readonly HttpClient _httpClient;
private readonly AmiiboWindow _owner;
private List<AmiiboApi> _amiiboList;
private AvaloniaList<AmiiboApi> _amiibos;
private ObservableCollection<string> _amiiboSeries;
public enum AmiiboSortField
{
Name,
}
private int _amiiboSelectedIndex;
private int _seriesSelectedIndex;
private bool _showAllAmiibo;
// ReSharper disable once InconsistentNaming
private static bool _cachedUseRandomUuid;
public bool IsSortedByName => _sortField == AmiiboSortField.Name;
private const string DefaultJson = "{ \"amiibo\": [] }";
private const float AmiiboImageSize = 350f;
public string TitleId { get; set; }
public string LastScannedAmiiboId { get; set; }
public string SortingFieldName => LocaleManager.Instance[LocaleKeys.Common_Sort_NameLabel];
private readonly string _amiiboJsonPath;
private readonly byte[] _amiiboLogoBytes;
private readonly HttpClient _httpClient;
private readonly AmiiboWindow _owner;
private List<AmiiboApi> _amiiboList;
private AvaloniaList<AmiiboApi> _amiibos;
private ObservableCollection<string> _amiiboSeries;
private CancellationTokenSource _imageCts = new();
private static readonly AmiiboJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions());
public AmiiboWindowViewModel(AmiiboWindow owner, string lastScannedAmiiboId, string titleId)
@@ -73,9 +83,6 @@ namespace Ryujinx.Ava.UI.ViewModels
public AmiiboWindowViewModel() { }
public string TitleId { get; set; }
public string LastScannedAmiiboId { get; set; }
public UserResult Response { get; private set; }
public bool UseRandomUuid
@@ -151,6 +158,42 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public bool PauseEmulationWhileScanningAmiibo
{
get => ConfigurationState.Instance.UI.PauseEmulationWhileScanningAmiibo.Value;
set
{
ConfigurationState.Instance.UI.PauseEmulationWhileScanningAmiibo.Value = value;
ConfigurationState.Instance.ToFileFormat().SaveConfig(Program.ConfigurationPath);
OnPropertyChanged();
}
}
private string _searchText = string.Empty;
private bool _sortingAscending = true;
public string SearchText
{
get => _searchText;
set
{
_searchText = value;
FilterAmiibo();
OnPropertyChanged();
}
}
public bool SortingAscending
{
get => _sortingAscending;
set
{
_sortingAscending = value;
FilterAmiibo();
OnPropertyChanged();
}
}
[ObservableProperty]
public partial Bitmap AmiiboImage { get; set; }
@@ -166,14 +209,14 @@ namespace Ryujinx.Ava.UI.ViewModels
{
_owner.ScannedAmiibo = AmiiboList[AmiiboSelectedIndex];
_owner.IsScanned = true;
_owner.Close();
}
}
public void Cancel()
{
_owner.IsScanned = false;
_owner.Close();
}
public void Dispose()
@@ -315,42 +358,46 @@ namespace Ryujinx.Ava.UI.ViewModels
private void ParseAmiiboData()
{
_amiiboSeries.Clear();
_amiibos.Clear();
for (int i = 0; i < _amiiboList.Count; i++)
foreach (AmiiboApi amiibo in _amiiboList)
{
if (!_amiiboSeries.Contains(_amiiboList[i].AmiiboSeries))
if (!_amiiboSeries.Contains(amiibo.AmiiboSeries))
{
if (!ShowAllAmiibo)
if (_showAllAmiibo)
{
foreach (AmiiboApiGamesSwitch game in _amiiboList[i].GamesSwitch)
{
if (game != null)
{
if (game.GameId.Contains(TitleId))
{
AmiiboSeries.Add(_amiiboList[i].AmiiboSeries);
break;
}
}
}
_amiiboSeries.Add(amiibo.AmiiboSeries);
}
else
{
AmiiboSeries.Add(_amiiboList[i].AmiiboSeries);
bool hasCompatible = amiibo.GamesSwitch.Any(game =>
game != null && game.GameId.Contains(TitleId));
if (hasCompatible)
{
_amiiboSeries.Add(amiibo.AmiiboSeries);
}
}
}
}
if (LastScannedAmiiboId != string.Empty)
if (_showAllAmiibo)
{
SeriesSelectedIndex = -1;
}
else if (LastScannedAmiiboId != string.Empty)
{
SelectLastScannedAmiibo();
}
else
else if (_amiiboSeries.Count > 0)
{
SeriesSelectedIndex = 0;
}
else
{
SeriesSelectedIndex = -1;
}
FilterAmiibo();
}
private void SelectLastScannedAmiibo()
@@ -360,104 +407,109 @@ namespace Ryujinx.Ava.UI.ViewModels
SeriesSelectedIndex = AmiiboSeries.IndexOf(scanned.AmiiboSeries);
AmiiboSelectedIndex = AmiiboList.IndexOf(scanned);
}
private void FilterAmiibo()
{
_amiibos.Clear();
List<AmiiboApi> amiiboSortedList;
IEnumerable<AmiiboApi> query = _amiiboList.AsEnumerable();
// If no series selected, just display all available amiibos
if (_seriesSelectedIndex < 0)
if (_seriesSelectedIndex >= 0 && _seriesSelectedIndex < _amiiboSeries.Count)
{
amiiboSortedList = _amiiboList
.OrderBy(amiibo => amiibo.AmiiboSeries)
.ThenBy(x => x.Name)
.ToList();
}
else
{
amiiboSortedList = _amiiboList
.Where(amiibo => amiibo.AmiiboSeries == _amiiboSeries[SeriesSelectedIndex])
.OrderBy(amiibo => amiibo.Name).ToList();
string selectedSeries = _amiiboSeries[_seriesSelectedIndex];
query = query.Where(amiibo => amiibo.AmiiboSeries == selectedSeries);
}
for (int i = 0; i < amiiboSortedList.Count; i++)
if (!string.IsNullOrWhiteSpace(_searchText))
{
if (!_amiibos.Contains(amiiboSortedList[i]))
query = query.Where(amiibo =>
amiibo.Name.Contains(_searchText, StringComparison.OrdinalIgnoreCase));
}
if (!_showAllAmiibo)
{
query = query.Where(amiibo =>
amiibo.GamesSwitch.Any(game => game != null && game.GameId.Contains(TitleId)));
}
query = _sortingAscending
? query.OrderBy(amiibo => amiibo.Name)
: query.OrderByDescending(amiibo => amiibo.Name);
_amiibos.AddRange(query);
int restoredIndex = -1;
for (int i = 0; i < _amiibos.Count; i++)
{
if (_amiibos[i].GetId() == LastScannedAmiiboId)
{
if (!_showAllAmiibo)
{
foreach (AmiiboApiGamesSwitch game in amiiboSortedList[i].GamesSwitch)
{
if (game != null)
{
if (game.GameId.Contains(TitleId))
{
_amiibos.Add(amiiboSortedList[i]);
break;
}
}
}
}
else
{
_amiibos.Add(amiiboSortedList[i]);
}
restoredIndex = i;
break;
}
}
AmiiboSelectedIndex = 0;
AmiiboSelectedIndex = restoredIndex != -1
? restoredIndex
: (_amiibos.Count > 0 ? 0 : -1);
SetAmiiboDetails();
}
private AmiiboSortField _sortField = AmiiboSortField.Name;
public AmiiboSortField SortingField
{
get => _sortField;
set
{
_sortField = value;
FilterAmiibo();
OnPropertyChanged(nameof(SortingFieldName));
OnPropertyChanged(nameof(IsSortedByName));
}
}
private void SetAmiiboDetails()
{
ResetAmiiboPreview();
Usage = string.Empty;
if (_amiiboSelectedIndex < 0 || _amiibos.Count < 1)
{
return;
}
AmiiboApi selected = _amiibos[_amiiboSelectedIndex];
string imageUrl = _amiiboList.FirstOrDefault(amiibo => amiibo.Equals(selected)).Image;
string imageUrl = selected.Image;
StringBuilder usageStringBuilder = new();
bool writable = false;
for (int i = 0; i < _amiiboList.Count; i++)
foreach (AmiiboApiGamesSwitch game in selected.GamesSwitch)
{
if (_amiiboList[i].Equals(selected))
if (game != null && game.GameId.Contains(TitleId))
{
bool writable = false;
foreach (AmiiboApiGamesSwitch item in _amiiboList[i].GamesSwitch)
foreach (AmiiboApiUsage usageItem in game.AmiiboUsage)
{
if (item.GameId.Contains(TitleId))
{
foreach (AmiiboApiUsage usageItem in item.AmiiboUsage)
{
usageStringBuilder.Append($"{Environment.NewLine}- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}");
writable = usageItem.Write;
}
}
usageStringBuilder.Append($"{Environment.NewLine}- {usageItem.Usage.Replace("/", Environment.NewLine + "-")}");
if (usageItem.Write)
writable = true;
}
if (usageStringBuilder.Length == 0)
{
usageStringBuilder.Append($"{LocaleManager.Instance[LocaleKeys.Unknown]}.");
}
Usage = $"{LocaleManager.Instance[LocaleKeys.Usage]} {(writable ? $" ({LocaleManager.Instance[LocaleKeys.Writable]})" : string.Empty)} : {usageStringBuilder}";
}
}
string usageLabel = writable
? LocaleManager.Instance[LocaleKeys.Amiibo_WritableLabel]
: LocaleManager.Instance[LocaleKeys.Amiibo_UsageLabel];
if (usageStringBuilder.Length == 0)
{
usageStringBuilder.Append(Environment.NewLine + Environment.NewLine + LocaleManager.Instance[LocaleKeys.Amiibo_UnknownLabel]);
}
else
{
usageStringBuilder.Replace(Environment.NewLine + "-", Environment.NewLine + Environment.NewLine + "-");
}
Usage = usageLabel + usageStringBuilder.ToString();
_ = UpdateAmiiboPreview(imageUrl);
}
@@ -510,8 +562,8 @@ namespace Ryujinx.Ava.UI.ViewModels
Logger.Error?.Print(LogClass.Application, $"Failed to request amiibo data: {exception}");
}
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogAmiiboApiTitle],
LocaleManager.Instance[LocaleKeys.DialogAmiiboApiFailFetchMessage],
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.Dialog_Amiibo_APITitle],
LocaleManager.Instance[LocaleKeys.Dialog_Amiibo_APIFailFetchMessage],
LocaleManager.Instance[LocaleKeys.InputDialogOk],
string.Empty,
LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
@@ -526,26 +578,33 @@ namespace Ryujinx.Ava.UI.ViewModels
private async Task UpdateAmiiboPreview(string imageUrl)
{
HttpResponseMessage response = await _httpClient.GetAsync(imageUrl);
_imageCts.Cancel();
_imageCts = new CancellationTokenSource();
if (response.IsSuccessStatusCode)
try
{
byte[] amiiboPreviewBytes = await response.Content.ReadAsByteArrayAsync();
using MemoryStream memoryStream = new(amiiboPreviewBytes);
using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(_imageCts.Token);
Bitmap bitmap = new(memoryStream);
HttpResponseMessage response = await _httpClient.GetAsync(imageUrl, linkedCts.Token);
double ratio = Math.Min(AmiiboImageSize / bitmap.Size.Width,
AmiiboImageSize / bitmap.Size.Height);
if (response.IsSuccessStatusCode)
{
byte[] bytes = await response.Content.ReadAsByteArrayAsync(linkedCts.Token);
using MemoryStream ms = new(bytes);
int resizeHeight = (int)(bitmap.Size.Height * ratio);
int resizeWidth = (int)(bitmap.Size.Width * ratio);
Bitmap bitmap = new(ms);
AmiiboImage = bitmap.CreateScaledBitmap(new PixelSize(resizeWidth, resizeHeight));
double ratio = Math.Min(AmiiboImageSize / bitmap.Size.Width, AmiiboImageSize / bitmap.Size.Height);
int newWidth = (int)(bitmap.Size.Width * ratio);
int newHeight = (int)(bitmap.Size.Height * ratio);
AmiiboImage = bitmap.CreateScaledBitmap(new PixelSize(newWidth, newHeight));
}
}
else
catch (OperationCanceledException) { }
catch (Exception ex)
{
Logger.Error?.Print(LogClass.Application, $"Failed to get amiibo preview. Response status code: {response.StatusCode}");
Logger.Error?.Print(LogClass.Application, $"Failed to load amiibo preview: {ex}");
}
}
@@ -560,8 +619,8 @@ namespace Ryujinx.Ava.UI.ViewModels
private static async Task ShowInfoDialog()
{
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.DialogAmiiboApiTitle],
LocaleManager.Instance[LocaleKeys.DialogAmiiboApiConnectErrorMessage],
await ContentDialogHelper.CreateInfoDialog(LocaleManager.Instance[LocaleKeys.Dialog_Amiibo_APITitle],
LocaleManager.Instance[LocaleKeys.Dialog_Amiibo_APIConnectErrorMessage],
LocaleManager.Instance[LocaleKeys.InputDialogOk],
string.Empty,
LocaleManager.Instance[LocaleKeys.RyujinxInfo]);
+106 -36
View File
@@ -160,6 +160,10 @@ namespace Ryujinx.Ava.UI.ViewModels
[ObservableProperty] public partial bool UpdateAvailable { get; set; }
[ObservableProperty] public partial string SkylanderButtonHeader { get; set; } = string.Empty;
[ObservableProperty] public partial bool SkylanderButtonEnabled { get; set; }
public static AsyncRelayCommand UpdateCommand { get; } = Commands.Create(async () =>
{
if (Updater.CanUpdate(true))
@@ -204,6 +208,8 @@ namespace Ryujinx.Ava.UI.ViewModels
internal AppHost AppHost { get; set; }
public RelayCommand ToggleSkylanderCommand { get; }
public MainWindowViewModel()
{
Applications = [];
@@ -233,6 +239,8 @@ namespace Ryujinx.Ava.UI.ViewModels
Volume = ConfigurationState.Instance.System.AudioVolume;
CustomVSyncInterval = ConfigurationState.Instance.Graphics.CustomVSyncInterval.Value;
}
ToggleSkylanderCommand = new RelayCommand(ExecuteToggleSkylander);
}
~MainWindowViewModel()
@@ -376,6 +384,50 @@ namespace Ryujinx.Ava.UI.ViewModels
public bool CanScanAmiiboBinaries => AmiiboBinReader.HasAmiiboKeyFile;
[RelayCommand]
private void UpdateSkylanderButton()
{
SkylanderButtonHeader = HasSkylander
? LocaleManager.Instance[LocaleKeys.MenuBar_Actions_RemoveSkylanderButton]
: LocaleManager.Instance[LocaleKeys.MenuBar_Actions_ScanSkylanderButton];
SkylanderButtonEnabled = HasSkylander || IsSkylanderRequested;
OnPropertyChanged(nameof(SkylanderButtonHeader));
OnPropertyChanged(nameof(SkylanderButtonEnabled));
Dispatcher.UIThread.Post(() =>
{
OnPropertyChanged(nameof(SkylanderButtonHeader));
}, DispatcherPriority.Render);
}
private void UpdateSkylanderState()
{
if (AppHost?.Device?.System == null) return;
IsSkylanderRequested = AppHost.Device.System.SearchingForSkylander(out _);
HasSkylander = AppHost.Device.System.HasSkylander(out _);
UpdateSkylanderButton();
}
private async void ExecuteToggleSkylander()
{
if (!IsGameRunning) return;
if (HasSkylander)
{
await RemoveSkylander();
}
else
{
await ScanSkylander();
}
UpdateSkylanderState();
}
public bool IsSkylanderRequested
{
get => field && _isGameRunning;
@@ -384,6 +436,7 @@ namespace Ryujinx.Ava.UI.ViewModels
field = value;
OnPropertyChanged();
UpdateSkylanderButton();
}
}
@@ -395,17 +448,7 @@ namespace Ryujinx.Ava.UI.ViewModels
field = value;
OnPropertyChanged();
}
}
public bool ShowSkylanderActions
{
get => field && _isGameRunning;
set
{
field = value;
OnPropertyChanged();
UpdateSkylanderButton();
}
}
@@ -1260,7 +1303,13 @@ namespace Ryujinx.Ava.UI.ViewModels
break;
case ShaderCacheLoadingState shaderCacheState:
CacheLoadStatus = $"{current} / {total}";
Splash = $"\"{SplashTextHelper.GetSplash()}\"";
string splash = SplashTextHelper.GetSplash();
if (!splash.IsNullOrEmpty())
{
Splash = $"\"{splash}\"";
}
switch (shaderCacheState)
{
case ShaderCacheLoadingState.Start:
@@ -1490,7 +1539,7 @@ namespace Ryujinx.Ava.UI.ViewModels
Title = LocaleManager.Instance[LocaleKeys.Dialog_Firmware_InstallFromFileFilePickerTitle],
FileTypeFilter = new List<FilePickerFileType>
{
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
new(LocaleManager.Instance[LocaleKeys.Common_FilePicker_AllSupportedFormats])
{
Patterns = ["*.xci", "*.zip"],
AppleUniformTypeIdentifiers = ["com.ryujinx.xci", "public.zip-archive"],
@@ -1674,7 +1723,7 @@ namespace Ryujinx.Ava.UI.ViewModels
Title = LocaleManager.Instance[LocaleKeys.Dialog_FileMenu_LoadApplicationFromFileFilePickerTitle],
FileTypeFilter = new List<FilePickerFileType>
{
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
new(LocaleManager.Instance[LocaleKeys.Common_FilePicker_AllSupportedFormats])
{
Patterns = ["*.nsp", "*.xci", "*.nca", "*.nro", "*.nso"],
AppleUniformTypeIdentifiers =
@@ -1761,7 +1810,7 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
private async Task<IReadOnlyList<string>?> PickFolders(LocaleKeys titleKey)
private async Task<IReadOnlyList<string>> PickFolders(LocaleKeys titleKey)
{
return (await StorageProvider.OpenMultiFolderPickerAsync(new FolderPickerOpenOptions
{
@@ -2004,40 +2053,58 @@ namespace Ryujinx.Ava.UI.ViewModels
}
}
public async Task OpenBinFile()
public async Task ScanAmiiboFromBin()
{
if (AppHost.Device.System.SearchingForAmiibo(out _) && IsGameRunning)
{
Optional<IStorageFile> result = await StorageProvider.OpenSingleFilePickerAsync(
new FilePickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.OpenFileDialogTitle],
FileTypeFilter = new List<FilePickerFileType>
{
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
{
Patterns = ["*.bin"],
}
}
});
bool shouldPause = ConfigurationState.Instance.UI.PauseEmulationWhileScanningAmiibo.Value && IsGameRunning;
if (result.HasValue)
if (shouldPause && AppHost?.Device?.System != null)
{
AppHost?.Pause();
}
try
{
if (AppHost.Device.System.SearchingForAmiibo(out _) && IsGameRunning)
{
AppHost.Device.System.ScanAmiiboFromBin(result.Value.Path.LocalPath);
Optional<IStorageFile> result = await StorageProvider.OpenSingleFilePickerAsync(
new FilePickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.Dialog_Amiibo_ScanAmiiboFromBinFilePickerTitle],
FileTypeFilter = new List<FilePickerFileType>
{
new(LocaleManager.Instance[LocaleKeys.Common_FilePicker_AllSupportedFormats])
{
Patterns = ["*.bin"],
}
}
});
if (result.HasValue)
{
AppHost.Device.System.ScanAmiiboFromBin(result.Value.Path.LocalPath);
}
}
}
finally
{
if (shouldPause && AppHost?.Device?.System != null)
{
AppHost?.Resume();
}
}
}
public async Task OpenSkylanderWindow()
[RelayCommand]
public async Task ScanSkylander()
{
if (AppHost.Device.System.SearchingForSkylander(out int deviceId))
{
Optional<IStorageFile> result = await StorageProvider.OpenSingleFilePickerAsync(
new FilePickerOpenOptions
{
Title = LocaleManager.Instance[LocaleKeys.OpenFileDialogTitle],
Title = LocaleManager.Instance[LocaleKeys.Dialog_Skylanders_ScanSkylanderFilePickerTitle],
FileTypeFilter = new List<FilePickerFileType>
{
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
new(LocaleManager.Instance[LocaleKeys.Common_FilePicker_AllSupportedFormats])
{
Patterns = ["*.sky", "*.bin", "*.dmp", "*.dump"],
},
@@ -2060,12 +2127,15 @@ namespace Ryujinx.Ava.UI.ViewModels
AppHost.Device.System.ScanSkylander(deviceId, data);
}
}
UpdateSkylanderButton();
}
}
[RelayCommand]
public async Task RemoveSkylander()
{
AppHost.Device.System.RemoveSkylander();
UpdateSkylanderButton();
}
public void ReloadRenderDocApi()
@@ -2155,7 +2225,7 @@ namespace Ryujinx.Ava.UI.ViewModels
Window.Position.X + (int)(Window.Bounds.Width / 2),
Window.Position.Y + (int)(Window.Bounds.Height / 2));
Avalonia.Platform.Screen? screen =
Avalonia.Platform.Screen screen =
Window.Screens.ScreenFromVisual(Window) ??
Window.Screens.ScreenFromPoint(windowCenter) ??
Window.Screens.Primary;
@@ -154,7 +154,7 @@ namespace Ryujinx.Ava.UI.ViewModels
AllowMultiple = true,
FileTypeFilter = new List<FilePickerFileType>
{
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
new(LocaleManager.Instance[LocaleKeys.Common_FilePicker_AllSupportedFormats])
{
Patterns = ["*.nsp"],
AppleUniformTypeIdentifiers = ["com.ryujinx.nsp"],
@@ -16,7 +16,7 @@ using static Ryujinx.Common.Utilities.XCIFileTrimmer;
namespace Ryujinx.Ava.UI.ViewModels
{
public class XCITrimmerViewModel : BaseModel
public class XciTrimmerViewModel : BaseModel
{
private const long BytesPerMb = 1024 * 1024;
@@ -49,7 +49,7 @@ namespace Ryujinx.Ava.UI.ViewModels
private int _processingTotal;
public XCITrimmerViewModel(MainWindowViewModel mainWindowViewModel)
public XciTrimmerViewModel(MainWindowViewModel mainWindowViewModel)
{
_logger = new XCITrimmerLog.TrimmerWindow(this);
_mainWindowViewModel = mainWindowViewModel;
@@ -299,9 +299,9 @@ namespace Ryujinx.Ava.UI.ViewModels
private class CompareXCITrimmerFiles : IComparer<XCITrimmerFileModel>
{
private readonly XCITrimmerViewModel _viewModel;
private readonly XciTrimmerViewModel _viewModel;
public CompareXCITrimmerFiles(XCITrimmerViewModel ViewModel)
public CompareXCITrimmerFiles(XciTrimmerViewModel ViewModel)
{
_viewModel = ViewModel;
}
@@ -190,7 +190,7 @@
MinWidth="90"
Margin="5"
Click="Close">
<TextBlock Text="{ext:Locale InputDialogCancel}" />
<TextBlock Text="{ext:Locale Common_Buttons_Cancel}" />
</Button>
</StackPanel>
</Panel>
@@ -158,7 +158,7 @@
MinWidth="90"
Margin="5"
Click="Close">
<TextBlock Text="{ext:Locale InputDialogCancel}" />
<TextBlock Text="{ext:Locale Common_Buttons_Cancel}" />
</Button>
</StackPanel>
</Panel>
@@ -145,7 +145,7 @@
Name="CancelButton"
MinWidth="90"
Click="Close">
<TextBlock Text="{ext:Locale InputDialogCancel}" />
<TextBlock Text="{ext:Locale Common_Buttons_Cancel}" />
</Button>
</StackPanel>
</Panel>
@@ -1,5 +1,5 @@
<UserControl
x:Class="Ryujinx.Ava.UI.Views.Dialog.XCITrimmerView"
x:Class="Ryujinx.Ava.UI.Views.Dialog.XciTrimmerView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@@ -11,7 +11,7 @@
xmlns:models="clr-namespace:Ryujinx.Ava.Common.Models"
Width="700"
Height="600"
x:DataType="viewModels:XCITrimmerViewModel"
x:DataType="viewModels:XciTrimmerViewModel"
Focusable="True"
mc:Ignorable="d">
<Grid Margin="25,10,25,0" RowDefinitions="Auto,Auto,*,Auto,Auto">
@@ -121,7 +121,7 @@
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
CornerRadius="5"
IsVisible="{Binding $parent[UserControl].DataContext.Processing}"
IsVisible="{Binding !$parent[UserControl].((viewModels:XciTrimmerViewModel)DataContext).Processing}"
Maximum="100"
Minimum="0"
Value="{Binding PercentageProgress}" />
@@ -131,7 +131,7 @@
HorizontalAlignment="Right"
VerticalAlignment="Center"
MaxLines="1"
IsVisible="{Binding !$parent[UserControl].DataContext.Processing}"
IsVisible="{Binding !$parent[UserControl].((viewModels:XciTrimmerViewModel)DataContext).Processing}"
Text="{Binding ., Converter={x:Static helpers:XCITrimmerFileSpaceSavingsConverter.Instance}}" />
</Grid>
</Grid>
@@ -173,7 +173,7 @@
<Binding Path="!Cancel" />
</MultiBinding>
</Button.IsVisible>
<TextBlock Text="{ext:Locale InputDialogCancel}" />
<TextBlock Text="{ext:Locale Common_Buttons_Cancel}" />
</Button>
<Button Name="CloseButton" MinWidth="90" Click="Close" IsVisible="{Binding !Processing}">
<TextBlock Text="{ext:Locale SettingsButtonClose}" />
@@ -182,4 +182,4 @@
</Grid>
</Panel>
</Grid>
</UserControl>
</UserControl>
@@ -11,16 +11,16 @@ using System.Threading.Tasks;
namespace Ryujinx.Ava.UI.Views.Dialog
{
public partial class XCITrimmerView : RyujinxControl<XCITrimmerViewModel>
public partial class XciTrimmerView : RyujinxControl<XciTrimmerViewModel>
{
public XCITrimmerView()
public XciTrimmerView()
{
InitializeComponent();
}
private void ToggleSelect(object sender, RoutedEventArgs e)
{
if (DataContext is XCITrimmerViewModel vm)
if (DataContext is XciTrimmerViewModel vm)
vm.ToggleSelect();
}
@@ -31,9 +31,9 @@ namespace Ryujinx.Ava.UI.Views.Dialog
PrimaryButtonText = string.Empty,
SecondaryButtonText = string.Empty,
CloseButtonText = string.Empty,
Content = new XCITrimmerView
Content = new XciTrimmerView
{
ViewModel = new XCITrimmerViewModel(RyujinxApp.MainWindow.ViewModel)
ViewModel = new XciTrimmerViewModel(RyujinxApp.MainWindow.ViewModel)
},
Title = LocaleManager.Instance[LocaleKeys.MenuBar_Actions_XCITrimmerButton]
};
@@ -58,7 +58,7 @@ namespace Ryujinx.Ava.UI.Views.Dialog
private void Close(object sender, RoutedEventArgs e)
{
((ContentDialog)Parent).Hide();
((ContentDialog)Parent!).Hide();
}
private void Cancel(Object sender, RoutedEventArgs e)
@@ -69,7 +69,7 @@ namespace Ryujinx.Ava.UI.Views.Dialog
public void Sort_Checked(object sender, RoutedEventArgs args)
{
if (sender is RadioButton { Tag: string sortField })
ViewModel.SortingField = Enum.Parse<XCITrimmerViewModel.SortField>(sortField);
ViewModel.SortingField = Enum.Parse<XciTrimmerViewModel.SortField>(sortField);
}
public void Order_Checked(object sender, RoutedEventArgs args)
@@ -137,7 +137,7 @@
InputGesture="Ctrl + A"
IsEnabled="{Binding IsAmiiboRequested}" />
<MenuItem
Command="{Binding OpenBinFile}"
Command="{Binding ScanAmiiboFromBin}"
AttachedToVisualTree="ScanBinAmiiboMenuItem_AttachedToVisualTree"
Header="{ext:Locale MenuBar_Actions_ScanAmiiboFromBinButton}"
Icon="{ext:Icon fa-solid fa-chess-rook}"
@@ -146,21 +146,12 @@
IsEnabled="{Binding IsAmiiboBinRequested}" />
<Separator IsVisible="{Binding CanScanAmiiboBinaries}"/>
<MenuItem
Command="{Binding OpenSkylanderWindow}"
AttachedToVisualTree="ScanSkylanderMenuItem_AttachedToVisualTree"
Header="{ext:Locale MenuBar_Actions_ScanSkylanderButton}"
Command="{Binding ToggleSkylanderCommand}"
AttachedToVisualTree="SkylanderMenuItem_AttachedToVisualTree"
Header="{Binding SkylanderButtonHeader}"
Icon="{ext:Icon fa-solid fa-dragon}"
IsVisible="{Binding ShowSkylanderActions}"
InputGesture="Ctrl + S"
IsEnabled="{Binding IsSkylanderRequested}" />
<MenuItem
Command="{Binding RemoveSkylander}"
AttachedToVisualTree="RemoveSkylanderMenuItem_AttachedToVisualTree"
Header="{ext:Locale MenuBar_Actions_RemoveSkylanderButton}"
Icon="{ext:Icon fa-solid fa-dragon}"
IsVisible="{Binding ShowSkylanderActions}"
InputGesture="Ctrl + D"
IsEnabled="{Binding HasSkylander}" />
IsEnabled="{Binding SkylanderButtonEnabled}" />
<MenuItem
Command="{Binding SimulateWakeUpMessage}"
Header="{ext:Locale MenuBar_Actions_SimulateWakeUpMessageButton}"
@@ -215,7 +206,7 @@
<Separator />
<MenuItem Header="{ext:Locale MenuBar_Actions_ToolsLabel}" Icon="{ext:Icon fa-solid fa-toolbox}">
<MenuItem
Name="MiiAppletMenuItem" Header="{ext:Locale MenuBar_Actions_MiiEditorButton}" Icon="{ext:Icon fa-solid fa-face-grin-wide}" />
Name="MiiEditorMenuItem" Header="{ext:Locale MenuBar_Actions_MiiEditorButton}" Icon="{ext:Icon fa-solid fa-face-grin-wide}" />
<MenuItem Name="XCITrimmerMenuItem" Header="{ext:Locale MenuBar_Actions_XCITrimmerButton}" Icon="{ext:Icon fa-solid fa-scissors}" />
</MenuItem>
</MenuItem>
@@ -36,14 +36,14 @@ namespace Ryujinx.Ava.UI.Views.Main
ToggleFileTypesMenuItem.ItemsSource = GenerateToggleFileTypeItems();
ChangeLanguageMenuItem.ItemsSource = GenerateLanguageMenuItems();
MiiAppletMenuItem.Command = Commands.Create(OpenMiiApplet);
MiiEditorMenuItem.Command = Commands.Create(OpenMiiEditor);
CloseRyujinxMenuItem.Command = Commands.Create(() => Window?.Close());
OpenSettingsMenuItem.Command = Commands.Create(OpenSettings);
PauseEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Pause());
ResumeEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.Resume());
StopEmulationMenuItem.Command = Commands.Create(() => ViewModel.AppHost?.ShowExitPrompt().OrCompleted());
RestartEmulationMenuItem.Command = Commands.Create(() => ViewModel.RestartEmulation());
XCITrimmerMenuItem.Command = Commands.Create(XCITrimmerView.Show);
XCITrimmerMenuItem.Command = Commands.Create(XciTrimmerView.Show);
AboutWindowMenuItem.Command = Commands.Create(AboutView.Show);
CompatibilityListMenuItem.Command = Commands.Create(() => CompatibilityListWindow.Show());
LdnGameListMenuItem.Command = Commands.Create(() => LdnGamesListWindow.Show());
@@ -150,11 +150,11 @@ namespace Ryujinx.Ava.UI.Views.Main
ViewModel.LoadConfigurableHotKeys();
}
public AppletMetadata MiiApplet => new(ViewModel.ContentManager, LocaleManager.Instance[LocaleKeys.MenuBar_Actions_MiiEditorButton], 0x0100000000001009);
public AppletMetadata MiiEditor => new(ViewModel.ContentManager, LocaleManager.Instance[LocaleKeys.MenuBar_Actions_MiiEditorButton], 0x0100000000001009);
public async Task OpenMiiApplet()
public async Task OpenMiiEditor()
{
if (!MiiApplet.CanStart(out ApplicationData appData, out BlitStruct<ApplicationControlProperty> nacpData))
if (!MiiEditor.CanStart(out ApplicationData appData, out BlitStruct<ApplicationControlProperty> nacpData))
return;
await ViewModel.LoadApplication(appData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen, nacpData);
@@ -172,18 +172,13 @@ namespace Ryujinx.Ava.UI.Views.Main
ViewModel.IsAmiiboBinRequested = ViewModel.IsAmiiboRequested && AmiiboBinReader.HasAmiiboKeyFile;
}
private void ScanSkylanderMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
private void SkylanderMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
if (sender is MenuItem)
{
ViewModel.IsSkylanderRequested = ViewModel.AppHost.Device.System.SearchingForSkylander(out _);
ViewModel.ShowSkylanderActions = string.Equals(ViewModel.AppHost.Device.Processes.ActiveApplication.ProgramIdText.ToUpper(), "0100CCC0002E6000");
}
private void RemoveSkylanderMenuItem_AttachedToVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
if (sender is MenuItem)
ViewModel.HasSkylander = ViewModel.AppHost.Device.System.HasSkylander(out _);
ViewModel.ShowSkylanderActions = string.Equals(ViewModel.AppHost.Device.Processes.ActiveApplication.ProgramIdText.ToUpper(), "0100CCC0002E6000");
}
}
private void ChangeWindowSize(string resolution)
@@ -44,7 +44,7 @@ namespace Ryujinx.Ava.UI.Views.Settings
return;
}
MainWindow? mainWindow = RyujinxApp.MainWindow;
MainWindow mainWindow = RyujinxApp.MainWindow;
if (mainWindow?.ViewModel?.AppHost?.NpadManager is not { } npadManager)
{
return;
@@ -69,7 +69,7 @@ namespace Ryujinx.Ava.UI.Views.User
AllowMultiple = false,
FileTypeFilter = new List<FilePickerFileType>
{
new(LocaleManager.Instance[LocaleKeys.AllSupportedFormats])
new(LocaleManager.Instance[LocaleKeys.Common_FilePicker_AllSupportedFormats])
{
Patterns = ["*.jpg", "*.jpeg", "*.png", "*.bmp"],
AppleUniformTypeIdentifiers = ["public.jpeg", "public.png", "com.microsoft.bmp"],
+126 -44
View File
@@ -7,6 +7,7 @@
xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels"
xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup"
xmlns:controls="clr-namespace:Ryujinx.Ava.UI.Controls"
xmlns:pt="using:Projektanker.Icons.Avalonia"
mc:Ignorable="d"
d:DesignWidth="400"
d:DesignHeight="350"
@@ -14,82 +15,163 @@
x:DataType="viewModels:AmiiboWindowViewModel"
CanResize="False"
WindowStartupLocation="CenterOwner"
Width="800"
Width="950"
MinHeight="650"
Height="650"
Height="700"
SizeToContent="Manual"
MinWidth="600"
Focusable="True">
<Design.DataContext>
<viewModels:AmiiboWindowViewModel />
</Design.DataContext>
<Grid RowDefinitions="Auto,Auto,*,Auto" Margin="15" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
<Grid Name="FlushControls" ColumnDefinitions="Auto,Auto" Grid.Row="1" Margin="-15">
<controls:RyujinxLogo
Grid.Column="0"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="8, 8, 7, 0"
ToolTip.Tip="{ext:WindowTitle Amiibo}" />
<StackPanel Grid.Column="1" Orientation="Horizontal" Margin="0, 0, 0, 20">
<StackPanel Spacing="10" Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock VerticalAlignment="Center" Text="{ext:Locale AmiiboSeriesLabel}" />
<ComboBox Margin="0, 8, 0, 0" SelectedIndex="{Binding SeriesSelectedIndex}" ItemsSource="{Binding AmiiboSeries}" MinWidth="100" />
</StackPanel>
<Grid Grid.Row="0" RowDefinitions="Auto,Auto,*,Auto" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<!-- UI FlushControls -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,Auto,Auto,Auto,Auto" Name="FlushControls">
<controls:RyujinxLogo Grid.Column="0" Margin="15,0,7,0" ToolTip.Tip="{ext:WindowTitle Amiibo_WindowTitle}" />
<TextBox Grid.Column="1"
Margin="0,5,0,7"
Width="250"
Watermark="{ext:Locale Common_Search_SearchAmiiboWatermark}"
Text="{Binding SearchText}" />
<StackPanel Grid.Column="2" Orientation="Horizontal">
<ComboBox Margin="10,5,0,5" SelectedIndex="{Binding SeriesSelectedIndex}" ItemsSource="{Binding AmiiboSeries}" MinWidth="140" />
</StackPanel>
<DropDownButton Grid.Column="3" Margin="10,5,0,5" Content="{Binding SortingFieldName}">
<DropDownButton.Flyout>
<Flyout Placement="Bottom">
<StackPanel Margin="0" HorizontalAlignment="Stretch" Orientation="Vertical">
<StackPanel>
<RadioButton GroupName="AmiiboSort" IsChecked="{Binding IsSortedByName}" Content="{ext:Locale Common_Sort_NameLabel}" Tag="Name" Checked="Sort_Checked" />
</StackPanel>
<Border Width="60" Height="2" Margin="5" HorizontalAlignment="Stretch" BorderBrush="White" BorderThickness="0,1,0,0">
<Separator Height="0" HorizontalAlignment="Stretch" />
</Border>
<RadioButton GroupName="AmiiboOrder" IsChecked="{Binding SortingAscending}" Content="{ext:Locale Common_Sort_OrderAscending}" Tag="Ascending" Checked="Order_Checked" />
<RadioButton GroupName="AmiiboOrder" IsChecked="{Binding !SortingAscending}" Content="{ext:Locale Common_Sort_OrderDescending}" Tag="Descending" Checked="Order_Checked" />
</StackPanel>
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>
<Button
Grid.Column="4"
Margin="10,5,0,5"
MinWidth="32"
MinHeight="32"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Click="SettingsButton_Click">
<pt:Icon Value="fa-solid fa-gear" />
<FlyoutBase.AttachedFlyout>
<Flyout Placement="Bottom">
<StackPanel>
<CheckBox
IsChecked="{Binding UseRandomUuid}">
<TextBlock Text="{ext:Locale Amiibo_UseRandomUuidLabel}" />
</CheckBox>
<CheckBox
IsChecked="{Binding PauseEmulationWhileScanningAmiibo}">
<TextBlock Text="{ext:Locale Amiibo_PauseEmulationWhileScanningAmiibo}" />
</CheckBox>
</StackPanel>
</Flyout>
</FlyoutBase.AttachedFlyout>
</Button>
<CheckBox Grid.Column="5" Margin="10,5,0,5" IsChecked="{Binding ShowAllAmiibo}">
<TextBlock Text="{ext:Locale Amiibo_ShowAllAmiiboLabel}" />
</CheckBox>
</Grid>
<Grid Name="NormalControls" ColumnDefinitions="*,*" Grid.Row="1" HorizontalAlignment="Stretch" >
<StackPanel Spacing="10" Orientation="Horizontal" HorizontalAlignment="Left">
<TextBlock VerticalAlignment="Center" Text="{ext:Locale AmiiboSeriesLabel}" />
<ComboBox SelectedIndex="{Binding SeriesSelectedIndex}" ItemsSource="{Binding AmiiboSeries}" MinWidth="100" />
<!-- UI NormalControls -->
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,Auto,Auto,Auto,Auto" Name="NormalControls">
<TextBox Grid.Column="0"
Margin="15,5,0,7"
Width="250"
Watermark="{ext:Locale Common_Search_SearchAmiiboWatermark}"
Text="{Binding SearchText}" />
<StackPanel Grid.Column="1" Orientation="Horizontal">
<ComboBox Margin="10,5,0,5" SelectedIndex="{Binding SeriesSelectedIndex}" ItemsSource="{Binding AmiiboSeries}" MinWidth="140" />
</StackPanel>
<DropDownButton Grid.Column="2" Margin="10,5,0,5" Content="{Binding SortingFieldName}">
<DropDownButton.Flyout>
<Flyout Placement="Bottom">
<StackPanel Margin="0" HorizontalAlignment="Stretch" Orientation="Vertical">
<StackPanel>
<RadioButton GroupName="AmiiboSort" IsChecked="{Binding IsSortedByName}" Content="{ext:Locale Common_Sort_NameLabel}" Tag="Name" Checked="Sort_Checked" />
</StackPanel>
<Border Width="60" Height="2" Margin="5" HorizontalAlignment="Stretch" BorderBrush="White" BorderThickness="0,1,0,0">
<Separator Height="0" HorizontalAlignment="Stretch" />
</Border>
<RadioButton GroupName="AmiiboOrder" IsChecked="{Binding SortingAscending}" Content="{ext:Locale Common_Sort_OrderAscending}" Tag="Ascending" Checked="Order_Checked" />
<RadioButton GroupName="AmiiboOrder" IsChecked="{Binding !SortingAscending}" Content="{ext:Locale Common_Sort_OrderDescending}" Tag="Descending" Checked="Order_Checked" />
</StackPanel>
</Flyout>
</DropDownButton.Flyout>
</DropDownButton>
<Button
Grid.Column="3"
Margin="10,5,0,5"
MinWidth="32"
MinHeight="32"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Click="SettingsButton_Click">
<pt:Icon Value="fa-solid fa-gear" />
<FlyoutBase.AttachedFlyout>
<Flyout Placement="Bottom">
<StackPanel>
<CheckBox
IsChecked="{Binding UseRandomUuid}">
<TextBlock Text="{ext:Locale Amiibo_UseRandomUuidLabel}" />
</CheckBox>
<CheckBox
IsChecked="{Binding PauseEmulationWhileScanningAmiibo}">
<TextBlock Text="{ext:Locale Amiibo_PauseEmulationWhileScanningAmiibo}" />
</CheckBox>
</StackPanel>
</Flyout>
</FlyoutBase.AttachedFlyout>
</Button>
<CheckBox Grid.Column="4" Margin="10,5,0,5" IsChecked="{Binding ShowAllAmiibo}">
<TextBlock Text="{ext:Locale Amiibo_ShowAllAmiiboLabel}" />
</CheckBox>
</Grid>
<DockPanel Grid.Row="2">
<!-- Amiibo list, images, and usage -->
<StackPanel Grid.Row="2" Margin="0,20,0,20" Orientation="Horizontal" HorizontalAlignment="Center" Spacing="50">
<ListBox
DockPanel.Dock="Left"
Width="300"
Margin="20"
Padding="5"
SelectionMode="Single"
ItemsSource="{Binding AmiiboList}"
SelectedIndex="{Binding AmiiboSelectedIndex}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Margin="10, 0, 0 ,0" Text="{Binding Name}" />
<TextBlock Margin="10,0,0,0" Text="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<StackPanel Margin="20">
<Image Source="{Binding AmiiboImage}" Height="350" Width="150" HorizontalAlignment="Center" />
<ScrollViewer MaxHeight="120" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto"
Margin="20" VerticalAlignment="Top" HorizontalAlignment="Stretch">
<TextBlock TextWrapping="Wrap" Text="{Binding Usage}" HorizontalAlignment="Center"
TextAlignment="Center" />
<StackPanel Width="300">
<Image Source="{Binding AmiiboImage}" Height="400" Width="200" />
<ScrollViewer MaxHeight="120" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" Margin="20" VerticalAlignment="Top" HorizontalAlignment="Stretch">
<TextBlock TextWrapping="Wrap" Text="{Binding Usage}" HorizontalAlignment="Center" TextAlignment="Center" />
</ScrollViewer>
</StackPanel>
</DockPanel>
<Grid Grid.Row="3" ColumnDefinitions="Auto,Auto,*,Auto,Auto">
<CheckBox Margin="10" Grid.Column="0" VerticalContentAlignment="Center"
IsChecked="{Binding ShowAllAmiibo}"
Content="{ext:Locale AmiiboOptionsShowAllLabel}" />
<CheckBox HotKey="H"
Margin="10" VerticalContentAlignment="Center" Grid.Column="1"
IsChecked="{Binding UseRandomUuid}"
Content="{ext:Locale AmiiboOptionsUsRandomTagLabel}" />
</StackPanel>
<Grid Grid.Row="3" Margin="0,0,10,10" ColumnDefinitions="Auto,Auto,*,Auto,Auto">
<Button Grid.Column="3"
IsEnabled="{Binding EnableScanning}"
Width="80"
Name="ScanButton"
HotKey="Return"
Content="{ext:Locale AmiiboScanButtonLabel}"
Content="{ext:Locale Amiibo_ScanButton}"
Click="ScanButton_Click" />
<Button Grid.Column="4"
Margin="10,0"
Margin="10,0,0,0"
Width="80"
Name="CancelButton"
HotKey="Escape"
Content="{ext:Locale InputDialogCancel}"
Content="{ext:Locale Common_Buttons_Cancel}"
Click="CancelButton_Click" />
</Grid>
</Grid>
</window:StyleableAppWindow>
</window:StyleableAppWindow>
+80 -4
View File
@@ -1,8 +1,12 @@
using Avalonia.Interactivity;
using Ryujinx.Ava.Common.Locale;
using Ryujinx.Ava.Common.Models.Amiibo;
using Ryujinx.Ava.Systems;
using Ryujinx.Ava.Systems.Configuration;
using Ryujinx.Ava.UI.ViewModels;
using Avalonia.Controls;
using System;
using Avalonia.Controls.Primitives;
namespace Ryujinx.Ava.UI.Windows
{
@@ -20,7 +24,50 @@ namespace Ryujinx.Ava.UI.Windows
FlushControls.IsVisible = !ConfigurationState.Instance.ShowOldUI;
NormalControls.IsVisible = ConfigurationState.Instance.ShowOldUI;
Title = RyujinxApp.FormatTitle(LocaleKeys.Amiibo);
Title = RyujinxApp.FormatTitle(LocaleKeys.Amiibo_WindowTitle);
if (ViewModel.PauseEmulationWhileScanningAmiibo && RyujinxApp.MainWindow?.ViewModel?.AppHost != null)
{
RyujinxApp.MainWindow.ViewModel.AppHost.Pause();
}
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
}
private void SettingsButton_Click(object sender, RoutedEventArgs e)
{
FlyoutBase.ShowAttachedFlyout((Control)sender!);
}
private void ViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(ViewModel.PauseEmulationWhileScanningAmiibo))
return;
AppHost host = RyujinxApp.MainWindow?.ViewModel?.AppHost;
if (host == null)
return;
if (ViewModel.PauseEmulationWhileScanningAmiibo)
host.Pause();
else
host.Resume();
}
private void AlwaysResumeOnClose()
{
if (RyujinxApp.MainWindow?.ViewModel?.AppHost != null)
{
RyujinxApp.MainWindow.ViewModel.AppHost.Resume();
}
}
protected override void OnClosed(EventArgs e)
{
ViewModel.PropertyChanged -= ViewModel_PropertyChanged;
AlwaysResumeOnClose();
base.OnClosed(e);
}
public AmiiboWindow()
@@ -31,7 +78,26 @@ namespace Ryujinx.Ava.UI.Windows
if (Program.PreviewerDetached)
{
Title = RyujinxApp.FormatTitle(LocaleKeys.Amiibo);
Title = RyujinxApp.FormatTitle(LocaleKeys.Amiibo_WindowTitle);
}
}
public void Sort_Checked(object sender, RoutedEventArgs e)
{
if (sender is RadioButton { Tag: string tag } && ViewModel != null)
{
if (Enum.TryParse<AmiiboWindowViewModel.AmiiboSortField>(tag, out var sortField))
{
ViewModel.SortingField = sortField;
}
}
}
public void Order_Checked(object sender, RoutedEventArgs e)
{
if (sender is RadioButton { Tag: string tag } && ViewModel != null)
{
ViewModel.SortingAscending = tag == "Ascending";
}
}
@@ -39,8 +105,18 @@ namespace Ryujinx.Ava.UI.Windows
public AmiiboApi ScannedAmiibo { get; set; }
public AmiiboWindowViewModel ViewModel;
private void ScanButton_Click(object sender, RoutedEventArgs e) => ViewModel.Scan();
private void ScanButton_Click(object sender, RoutedEventArgs e)
{
AlwaysResumeOnClose();
ViewModel.Scan();
Close();
}
private void CancelButton_Click(object sender, RoutedEventArgs e) => ViewModel.Cancel();
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
AlwaysResumeOnClose();
ViewModel.Cancel();
Close();
}
}
}
+1 -1
View File
@@ -152,7 +152,7 @@
Margin="5"
HotKey="Escape"
Command="{Binding Close}">
<TextBlock Text="{ext:Locale InputDialogCancel}" />
<TextBlock Text="{ext:Locale Common_Buttons_Cancel}" />
</Button>
</DockPanel>
</DockPanel>
+2 -1
View File
@@ -40,10 +40,11 @@
<KeyBinding Gesture="F9" Command="{Binding ToggleDockMode}" />
<KeyBinding Gesture="Escape" Command="{Binding ExitCurrentState}" />
<KeyBinding Gesture="Ctrl+A" Command="{Binding OpenAmiiboWindow}" />
<KeyBinding Gesture="Ctrl+B" Command="{Binding OpenBinFile}" />
<KeyBinding Gesture="Ctrl+B" Command="{Binding ScanAmiiboFromBin}" />
<KeyBinding Gesture="Ctrl+R" Command="{Binding RestartEmulation}" />
<KeyBinding Gesture="Ctrl+Shift+R" Command="{Binding ReloadRenderDocApi}" />
<KeyBinding Gesture="Ctrl+Shift+C" Command="{Binding ToggleCapture}" />
<KeyBinding Gesture="Ctrl+S" Command="{Binding ToggleSkylanderCommand}" />
<KeyBinding Gesture="Ctrl+M" Command="{Binding SimulateWakeUpMessage}" />
<KeyBinding Gesture="Cmd+O" Command="{Binding LoadApplicationFromFile}" />
<KeyBinding Gesture="Ctrl+O" Command="{Binding LoadApplicationFromFile}" />
+1 -1
View File
@@ -155,7 +155,7 @@
Command="{Binding OkButton}" />
<Button
HotKey="Escape"
Content="{ext:Locale SettingsButtonCancel}"
Content="{ext:Locale Common_Buttons_Cancel}"
Command="{Binding CancelButton}" />
<Button
Content="{ext:Locale SettingsButtonApply}"