From fd6648e30a4afaf220f8e5c3a08648fbeca13900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hack=E8=8C=B6=E3=82=93?= Date: Sat, 2 Aug 2025 20:29:50 -0500 Subject: [PATCH 01/24] Update Korean translation (ryubing/ryujinx!100) See merge request ryubing/ryujinx!100 --- assets/locales.json | 48 ++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/assets/locales.json b/assets/locales.json index ae896da30..5849a25e3 100644 --- a/assets/locales.json +++ b/assets/locales.json @@ -754,7 +754,7 @@ "he_IL": "דמה הודעת השכמה", "it_IT": "Simula messaggio Wake-up", "ja_JP": "スリープ復帰メッセージをシミュレート", - "ko_KR": "웨이크업 메시지 시뮬레이션", + "ko_KR": "절전 모드 해제 메시지 시뮬레이션", "no_NO": "Simuler oppvåknings-melding", "pl_PL": "Symuluj wiadomość wybudzania", "pt_BR": "Simular Mensagem de Acordar o Console", @@ -10754,7 +10754,7 @@ "he_IL": "", "it_IT": "Pulsante dorsale sinistro", "ja_JP": "", - "ko_KR": "좌측 숄더", + "ko_KR": "좌측 범퍼", "no_NO": "Venstre Skulder", "pl_PL": "", "pt_BR": "Ombro Esquerdo", @@ -10779,7 +10779,7 @@ "he_IL": "", "it_IT": "Pulsante dorsale destro", "ja_JP": "", - "ko_KR": "우측 숄더", + "ko_KR": "우측 범퍼", "no_NO": "Høyre Skulder", "pl_PL": "", "pt_BR": "Ombro Direito", @@ -12329,7 +12329,7 @@ "he_IL": "העצמת רטט חזק", "it_IT": "Moltiplicatore vibrazione forte", "ja_JP": "強振動の補正値", - "ko_KR": "강력한 진동 증폭기", + "ko_KR": "강진동 보정폭", "no_NO": "Sterk Vibrasjon multiplikator", "pl_PL": "Mnożnik mocnych wibracji", "pt_BR": "Multiplicador de Vibração Forte", @@ -12354,7 +12354,7 @@ "he_IL": "מכפיל רטט חלש", "it_IT": "Moltiplicatore vibrazione debole", "ja_JP": "弱振動の補正値", - "ko_KR": "약한 진동 증폭기", + "ko_KR": "약진동 보정폭", "no_NO": "Svak Vibrasjon multiplikator", "pl_PL": "Mnożnik słabych wibracji", "pt_BR": "Multiplicador de Vibração Fraca", @@ -12954,7 +12954,7 @@ "he_IL": "אנא המתן, המחקה מתארגן מחדש", "it_IT": "Attendere prego, l'emulatore si sta riavviando", "ja_JP": "お待ちください、エミュレーターが再起動しています", - "ko_KR": "잠시만 기다려 주세요, 에뮬레이터가 재시작 중입니다", + "ko_KR": "잠시만 기다려 주세요, 에뮬레이터가 다시 시작 중입니다", "no_NO": "Vennligst vent, emulatoren starter på nytt", "pl_PL": "Proszę czekać, emulator jest w trakcie ponownego uruchamiania", "pt_BR": "Por favor, aguarde, o emulador está reiniciando", @@ -13029,7 +13029,7 @@ "he_IL": "מוסיף עדכון חדש...", "it_IT": "Aggiunta del nuovo aggiornamento...", "ja_JP": "新規アップデートを追加中...", - "ko_KR": "새 업데이트 추가 중...", + "ko_KR": "신규 업데이트 추가 중...", "no_NO": "Legger til ny oppdatering...", "pl_PL": "Dodawanie Nowej Aktualizacji...", "pt_BR": "Adicionando Nova Atualização...", @@ -16854,7 +16854,7 @@ "he_IL": "שומר זכרון מטמון של הצללות, דבר שמפחית תקיעות בריצות מסוימות.\n\nמוטב להשאיר דלוק אם לא בטוחים.", "it_IT": "Salva una cache degli shader su disco che riduce i fenomeni di stuttering nelle esecuzioni successive.\n\nNel dubbio, lascia l'opzione attiva.", "ja_JP": "ディスクシェーダーキャッシュをセーブし,次回以降の実行時遅延を軽減します.\n\nよくわからない場合はオンのままにしてください.", - "ko_KR": "후속 실행 시 끊김 현상을 줄이는 디스크 셰이더 캐시를 저장합니다.\n\n모르면 체크 상태로 두세요.", + "ko_KR": "후속 실행 시, 끊김 현상을 줄이는 디스크 셰이더 캐시를 저장합니다.\n\n모르면 체크 상태로 두세요.", "no_NO": "Lagrer en disk shader cache som reduserer hakking jo flere ganger du spiller.\n\nLa være PÅ om usikker.", "pl_PL": "Zapisuje pamięć podręczną shaderów na dysku, co zmniejsza zacinanie się w kolejnych uruchomieniach.\n\nPozostaw WŁĄCZONE, jeśli nie masz pewności.", "pt_BR": "Salva um cache de shader no disco que reduz a trepidação em execuções subsequentes.\n\nDeixe LIGADO se não tiver certeza.", @@ -16904,7 +16904,7 @@ "he_IL": "שיפור רזולוציית נקודה צפה, כגון 1.5. הוא שיפור לא אינטגרלי הנוטה לגרום יותר בעיות או להקריס.", "it_IT": "Scala della risoluzione in virgola mobile, come 1,5. I valori non interi hanno maggiori probabilità di causare problemi o arresti anomali.", "ja_JP": "1.5 のような整数でない倍率を指定すると,問題が発生したりクラッシュしたりする場合があります.", - "ko_KR": "부동 소수점 해상도 스케일(예: 1.5)입니다. 적분이 아닌 스케일은 문제나 충돌을 일으킬 가능성이 높습니다.", + "ko_KR": "부동 소수점 해상도 스케일(예 : 1.5)입니다. 적분이 아닌 스케일은 문제나 충돌을 일으킬 가능성이 높습니다.", "no_NO": "Det er mer sannsynlig at flytende punktoppløsning skalaer som 1.5. Ikke-integrerte skalaer forårsaker problemer eller krasj.", "pl_PL": "Skala rozdzielczości zmiennoprzecinkowej, np. 1,5. Skale niecałkowite częściej powodują problemy lub awarie.", "pt_BR": "Escala de resolução de ponto flutuante, como 1.5. Valores não inteiros tem probabilidade maior de causar problemas ou quebras.", @@ -17204,7 +17204,7 @@ "he_IL": "השתמש בזהירות", "it_IT": "Usa con cautela", "ja_JP": "使用上の注意", - "ko_KR": "주의해서 사용", + "ko_KR": "사용상 주의 요함", "no_NO": "Bruk med forsiktighet", "pl_PL": "Używaj ostrożnie", "pt_BR": "Use com cuidado", @@ -18779,7 +18779,7 @@ "he_IL": "", "it_IT": "Risparmia {0:n0} MB", "ja_JP": "", - "ko_KR": "{0:n0} Mb 저장", + "ko_KR": "{0:n0}MB 저장", "no_NO": "Spare {0:n0} Mb", "pl_PL": "", "pt_BR": "Salvar {0:n0} Mb", @@ -18804,7 +18804,7 @@ "he_IL": "", "it_IT": "Risparmiati {0:n0} MB", "ja_JP": "", - "ko_KR": "{0:n0}Mb 저장됨", + "ko_KR": "{0:n0}MB 저장됨", "no_NO": "Spart {0:n0} Mb", "pl_PL": "", "pt_BR": "Salvo {0:n0} Mb", @@ -19104,7 +19104,7 @@ "he_IL": "מחויב להיות ללא אותיות CJK", "it_IT": "Può contenere solo caratteri non CJK", "ja_JP": "CJK文字以外のみ", - "ko_KR": "CJK 문자가 아닌 문자만 가능", + "ko_KR": "한중일 문자 입력 불가", "no_NO": "Må kun være uten CJK-tegn", "pl_PL": "Nie może zawierać znaków CJK", "pt_BR": "Apenas devem ser caracteres não CJK.", @@ -19304,7 +19304,7 @@ "he_IL": "מוסיף קבצים חדשים...", "it_IT": "Aggiunta dei nuovi file...", "ja_JP": "新規ファイルを追加中...", - "ko_KR": "새 파일 추가...", + "ko_KR": "신규 파일 추가...", "no_NO": "Legger til nye filer...", "pl_PL": "Dodawanie Nowych Plików...", "pt_BR": "Adicionando Novos Arquivos...", @@ -19704,7 +19704,7 @@ "he_IL": "אתחול {0}", "it_IT": "Riavvio di {0}", "ja_JP": "{0} 再起動", - "ko_KR": "{0} 재부팅", + "ko_KR": "{0} 다시 시작", "no_NO": "Omstart av {0}", "pl_PL": "Ponowne uruchomienie {0}", "pt_BR": "Reinício de {0}", @@ -20954,7 +20954,7 @@ "he_IL": "", "it_IT": "Spazio risparmiato", "ja_JP": "", - "ko_KR": "공간 절약s", + "ko_KR": "공간 절약", "no_NO": "Plassbesparelser", "pl_PL": "", "pt_BR": "Economia de Espaço", @@ -21029,7 +21029,7 @@ "he_IL": "", "it_IT": "{0} nuovo/i aggiornamento/i aggiunto/i", "ja_JP": "", - "ko_KR": "{0}개의 새 업데이트가 추가됨", + "ko_KR": "{0}개의 신규 업데이트가 추가됨", "no_NO": "{0} ny(e) oppdatering(er) lagt til", "pl_PL": "", "pt_BR": "{0} nova(s) atualização(ões) adicionada(s)", @@ -21104,7 +21104,7 @@ "he_IL": "מזהה בניה:", "it_IT": "ID Build", "ja_JP": "ビルドID:", - "ko_KR": "빌드ID:", + "ko_KR": "빌드 ID:", "no_NO": "VersjonsId:", "pl_PL": "Identyfikator wersji:", "pt_BR": "ID da Build:", @@ -21254,7 +21254,7 @@ "he_IL": "", "it_IT": "{0} nuovo/i aggiornamento/i aggiunto/i", "ja_JP": "", - "ko_KR": "{0}개의 새 업데이트가 추가됨", + "ko_KR": "{0}개의 신규 업데이트가 추가됨", "no_NO": "{0} ny(e) oppdatering(er) lagt til", "pl_PL": "", "pt_BR": "{0} nova(s) atualização(ões) adicionada(s)", @@ -21979,7 +21979,7 @@ "he_IL": "הגבר את עוצמת הקול:", "it_IT": "Alza il volume:", "ja_JP": "音量を上げる:", - "ko_KR": "음량 증가 :", + "ko_KR": "소리 크게 :", "no_NO": "Øk Volum:", "pl_PL": "Zwiększ Głośność:", "pt_BR": "Aumentar Volume:", @@ -22004,7 +22004,7 @@ "he_IL": "הנמך את עוצמת הקול:", "it_IT": "Abbassa il volume:", "ja_JP": "音量を下げる:", - "ko_KR": "음량 감소 :", + "ko_KR": "소리 작게 :", "no_NO": "Reduser Volum:", "pl_PL": "Zmniejsz Głośność:", "pt_BR": "Diminuir Volume:", @@ -22804,7 +22804,7 @@ "he_IL": "ערוך משתמש", "it_IT": "Modifica utente", "ja_JP": "ユーザを編集", - "ko_KR": "사용자 편집", + "ko_KR": "아이디 편집", "no_NO": "Rediger bruker", "pl_PL": "Edytuj użytkownika", "pt_BR": "Editar Usuário", @@ -22829,7 +22829,7 @@ "he_IL": "צור משתמש", "it_IT": "Crea utente", "ja_JP": "ユーザを作成", - "ko_KR": "사용자 만들기", + "ko_KR": "아이디 만들기", "no_NO": "Opprett bruker", "pl_PL": "Utwórz użytkownika", "pt_BR": "Criar usuário", @@ -23054,7 +23054,7 @@ "he_IL": "", "it_IT": "Cambia la modalità multigiocatore LDN.\n\nLdnMitm modificherà la funzionalità locale wireless/local play nei giochi per funzionare come se fosse in modalità LAN, consentendo connessioni locali sulla stessa rete con altre istanze di Ryujinx e console Nintendo Switch modificate che hanno il modulo ldn_mitm installato.\n\nLa modalità multigiocatore richiede che tutti i giocatori usino la stessa versione del gioco (es. Super Smash Bros. Ultimate v13.0.1 non può connettersi con la v13.0.0).\n\nNel dubbio, lascia l'opzione su Disabilitato.", "ja_JP": "LDNマルチプレイヤーモードを変更します.\n\nldn_mitmモジュールがインストールされた, 他のRyujinxインスタンスや,ハックされたNintendo Switchコンソールとのローカル/同一ネットワーク接続を可能にします.\n\nマルチプレイでは, すべてのプレイヤーが同じゲームバージョンである必要があります(例:Super Smash Bros. Ultimate v13.0.1はv13.0.0に接続できません).\n\n不明な場合は「無効」のままにしてください.", - "ko_KR": "LDN 멀티플레이어 모드를 변경합니다.\n\nLdnMitm은 게임의 로컬 무선/로컬 플레이 기능을 LAN처럼 작동하도록 수정하여 다른 Ryujinx 인스턴스나 ldn_mitm 모듈이 설치된 해킹된 Nintendo Switch 콘솔과 로컬, 동일 네트워크 연결이 가능합니다.\n\n멀티플레이어는 모든 플레이어가 동일한 게임 버전을 사용해야 합니다(예: Super Smash Bros. Ultimate v13.0.1은 v13.0.0에 연결할 수 없음).\n\n모르면 비활성화 상태로 두세요.", + "ko_KR": "LDN 멀티플레이어 모드를 변경합니다.\n\nLdnMitm은 게임의 로컬 무선/로컬 플레이 기능을 LAN처럼 작동하도록 수정하여 다른 Ryujinx 인스턴스나 ldn_mitm 모듈이 설치된 해킹된 Nintendo Switch 콘솔과 로컬, 동일 네트워크 연결이 가능합니다.\n\n멀티플레이어는 모든 플레이어가 동일한 게임 버전을 사용해야 합니다(예 : 슈퍼 스매시브라더스 얼티밋 v13.0.1은 v13.0.0에 연결할 수 없음).\n\n모르면 비활성화 상태로 두세요.", "no_NO": "Endre LDN flerspillermodus.\n\nLdnMitm vil endre lokal trådløst/lokal spillfunksjonalitet i spill som skal fungere som om den var LAN, noe som tillater lokal, samme nettverk forbindelser med andre Ryujinx instanser og hacket Nintendo Switch konsoller som har installert ldn_mitm-modulen.\n\nFlerspiller krever at alle spillerne er på samme versjon (dvs. Super Smash Bros. Ultimat v13.0.1 kan ikke koble til v13.0.0).\n\nForlat DEAKTIVERT hvis usikker.", "pl_PL": "", "pt_BR": "Alterar o modo multiplayer LDN.\n\nLdnMitm modificará a funcionalidade de jogo sem fio/local nos jogos para funcionar como se fosse LAN, permitindo conexões locais, na mesma rede, com outras instâncias do Ryujinx e consoles Nintendo Switch hackeados que possuem o módulo ldn_mitm instalado.\n\nO multiplayer exige que todos os jogadores estejam na mesma versão do jogo (ex.: Super Smash Bros. Ultimate v13.0.1 não consegue se conectar à v13.0.0).\n\nDeixe DESATIVADO se estiver em dúvida.", From 31870707cf12c91e7243429ab6ea06ca9a232ae0 Mon Sep 17 00:00:00 2001 From: Neo Date: Sat, 2 Aug 2025 20:30:54 -0500 Subject: [PATCH 02/24] Tooltip Fix Pt.3 (ryubing/ryujinx!96) See merge request ryubing/ryujinx!96 --- assets/locales.json | 175 ------------------ .../UI/Controls/ApplicationContextMenu.axaml | 6 +- .../UI/Views/Main/MainMenuBarView.axaml | 6 +- .../UI/Views/Main/MainStatusBarView.axaml | 1 - .../UI/Views/Main/MainViewControls.axaml | 4 +- .../UI/Views/Settings/SettingsAudioView.axaml | 4 +- .../Views/Settings/SettingsSystemView.axaml | 1 + 7 files changed, 7 insertions(+), 190 deletions(-) diff --git a/assets/locales.json b/assets/locales.json index 5849a25e3..15f4d9ea4 100644 --- a/assets/locales.json +++ b/assets/locales.json @@ -17442,56 +17442,6 @@ "zh_TW": "停止模擬目前遊戲,返回遊戲選擇介面" } }, - { - "ID": "CheckUpdatesTooltip", - "Translations": { - "ar_SA": "التحقق من وجود تحديثات لريوجينكس", - "de_DE": "Sucht nach Updates für Ryujinx", - "el_GR": "Ελέγξτε για ενημερώσεις του Ryujinx", - "en_US": "Check for updates to Ryujinx", - "es_ES": "Busca actualizaciones para Ryujinx", - "fr_FR": "Vérifier les mises à jour de Ryujinx", - "he_IL": "בדוק אם קיימים עדכונים לריוג'ינקס", - "it_IT": "Controlla la presenza di aggiornamenti di Ryujinx", - "ja_JP": "Ryujinx のアップデートを確認します", - "ko_KR": "Ryujinx 업데이트 확인", - "no_NO": "Se etter oppdateringer til Ryujinx", - "pl_PL": "Sprawdź aktualizacje Ryujinx", - "pt_BR": "Verificar por atualizações para o Ryujinx", - "ru_RU": "Проверяет наличие обновлений Ryujinx", - "sv_SE": "Leta efter uppdateringar för Ryujinx", - "th_TH": "ตรวจสอบอัปเดตของ Ryujinx", - "tr_TR": "Ryujinx güncellemelerini denetlemeyi sağlar", - "uk_UA": "Перевіряє наявність оновлень для Ryujinx", - "zh_CN": "检查 Ryujinx 新版本", - "zh_TW": "檢查 Ryujinx 的更新" - } - }, - { - "ID": "OpenAboutTooltip", - "Translations": { - "ar_SA": "فتح حول النافذة", - "de_DE": "Öffnet das 'Über Ryujinx'-Fenster", - "el_GR": "Ανοίξτε το Παράθυρο Σχετικά", - "en_US": "Open About Window", - "es_ES": "Abre la ventana \"Acerca de\"", - "fr_FR": "Ouvrir la fenêtre À Propos", - "he_IL": "פתח את חלון אודות היישום", - "it_IT": "Apri la finestra delle informazioni", - "ja_JP": "Ryujinx についてのウインドウを開きます", - "ko_KR": "정보 창 열기", - "no_NO": "Åpne Om Vindu", - "pl_PL": "Otwórz Okno Informacje", - "pt_BR": "Abrir Janela Sobre", - "ru_RU": "Открывает окно «О программе»", - "sv_SE": "Öppna Om-fönstret", - "th_TH": "เปิดหน้าต่าง เกี่ยวกับ", - "tr_TR": "Hakkında penceresini açar", - "uk_UA": "Відкриває вікно «Про застосунок»", - "zh_CN": "打开关于窗口", - "zh_TW": "開啟關於視窗" - } - }, { "ID": "GridSize", "Translations": { @@ -17517,31 +17467,6 @@ "zh_TW": "網格尺寸" } }, - { - "ID": "GridSizeTooltip", - "Translations": { - "ar_SA": "تغيير حجم عناصر الشبكة", - "de_DE": "Ändert die Größe der Rasterelemente", - "el_GR": "Αλλαγή μεγέθους στοιχείων πλέγματος", - "en_US": "Change the size of grid items", - "es_ES": "Cambia el tamaño de los objetos en la cuadrícula", - "fr_FR": "Modifier la taille des éléments de la grille", - "he_IL": "שנה את גודל המוצרים על הרשת.", - "it_IT": "Cambia la dimensione dei riquadri della griglia", - "ja_JP": "グリッドサイズを変更します", - "ko_KR": "그리드 항목의 크기 변경", - "no_NO": "Endre størrelsen på rutenettet elementer", - "pl_PL": "Zmień rozmiar elementów siatki", - "pt_BR": "Mudar tamanho dos items da grade", - "ru_RU": "Меняет размер сетки элементов", - "sv_SE": "Ändra objektstorleken för rutnätet", - "th_TH": "เปลี่ยนขนาด ของตาราง", - "tr_TR": "Grid ögelerinin boyutunu değiştirmeyi sağlar", - "uk_UA": "Змінити розмір елементів сітки", - "zh_CN": "调整网格项目的大小", - "zh_TW": "調整網格的大小" - } - }, { "ID": "SettingsTabSystemSystemLanguageBrazilianPortuguese", "Translations": { @@ -17617,31 +17542,6 @@ "zh_TW": "音量:" } }, - { - "ID": "AudioVolumeTooltip", - "Translations": { - "ar_SA": "تغيير مستوى الصوت", - "de_DE": "Ändert die Lautstärke", - "el_GR": "Αλλαγή Έντασης Ήχου", - "en_US": "Change Audio Volume", - "es_ES": "Ajusta el nivel de volumen", - "fr_FR": "Modifier le volume audio", - "he_IL": "שנה עוצמת קול", - "it_IT": "Cambia volume audio", - "ja_JP": "音量を変更します", - "ko_KR": "음량 변경", - "no_NO": "Endre lydenivå", - "pl_PL": "Zmień Głośność Dźwięku", - "pt_BR": "Mudar Volume do Áudio", - "ru_RU": "Изменяет громкость звука", - "sv_SE": "Ändra ljudvolym", - "th_TH": "ปรับระดับเสียง", - "tr_TR": "Ses seviyesini değiştirir", - "uk_UA": "Регулювання гучності", - "zh_CN": "调节音量", - "zh_TW": "調節音量" - } - }, { "ID": "SettingsTabSystemEnableInternetAccess", "Translations": { @@ -17692,31 +17592,6 @@ "zh_TW": "允許模擬應用程式連線網際網路。\n\n當啟用此功能且系統連線到同一接入點時,具有區域網路模式的遊戲可相互連線。這也包括真正的遊戲機。\n\n不允許連接 Nintendo 伺服器。可能會導致某些嘗試連線網際網路的遊戲崩潰。\n\n如果不確定,請保持關閉狀態。" } }, - { - "ID": "GameListContextMenuManageCheatToolTip", - "Translations": { - "ar_SA": "إدارة الغش", - "de_DE": "Öffnet den Cheat-Manager", - "el_GR": "Διαχείριση Κόλπων", - "en_US": "Manage Cheats", - "es_ES": "Activa o desactiva los cheats", - "fr_FR": "Gérer les cheats", - "he_IL": "נהל צ'יטים", - "it_IT": "Gestisci trucchi", - "ja_JP": "チートを管理します", - "ko_KR": "치트 관리", - "no_NO": "Administrer juksemoduser", - "pl_PL": "Zarządzaj Kodami", - "pt_BR": "Gerenciar Trapaças", - "ru_RU": "Открывает окно управления читами", - "sv_SE": "Hantera fusk", - "th_TH": "ฟังก์ชั่นจัดการสูตรโกง", - "tr_TR": "Hileleri yönetmeyi sağlar", - "uk_UA": "Відкриває меню керування чит-кодами (cheats)", - "zh_CN": "管理当前游戏的金手指", - "zh_TW": "管理密技" - } - }, { "ID": "GameListContextMenuManageCheat", "Translations": { @@ -17742,31 +17617,6 @@ "zh_TW": "管理密技" } }, - { - "ID": "GameListContextMenuManageModToolTip", - "Translations": { - "ar_SA": "إدارة التعديلات", - "de_DE": "Mods verwalten", - "el_GR": "", - "en_US": "Manage Mods", - "es_ES": "Gestionar Mods", - "fr_FR": "Gérer les mods", - "he_IL": "נהל מודים", - "it_IT": "Gestisci mod", - "ja_JP": "Modを管理します", - "ko_KR": "모드 관리", - "no_NO": "Administrer modifikasjoner", - "pl_PL": "Zarządzaj modyfikacjami", - "pt_BR": "Gerenciar Mods", - "ru_RU": "Открывает окно управления модами", - "sv_SE": "Hantera moddar", - "th_TH": "ฟังก์ชั่นจัดการม็อด", - "tr_TR": "Modları Yönet", - "uk_UA": "Відкриває меню керування модифікаціями (mods)", - "zh_CN": "管理当前游戏的 MOD", - "zh_TW": "管理模組" - } - }, { "ID": "GameListContextMenuManageMod", "Translations": { @@ -18167,31 +18017,6 @@ "zh_TW": "圖示大小" } }, - { - "ID": "IconSizeTooltip", - "Translations": { - "ar_SA": "تغيير حجم أيقونات اللعبة", - "de_DE": "Ändert die Größe der Spiel-Cover", - "el_GR": "Αλλάξτε μέγεθος εικονιδίων των παιχνιδιών", - "en_US": "Change the size of game icons", - "es_ES": "Cambia el tamaño de los iconos de juegos", - "fr_FR": "Changer la taille des icônes de jeu", - "he_IL": "שנה את גודל הסמלים של משחקים", - "it_IT": "Cambia le dimensioni delle icone dei giochi", - "ja_JP": "ゲームアイコンのサイズを変更します", - "ko_KR": "게임 아이콘 크기 변경", - "no_NO": "Endre størrelsen på spillikonene", - "pl_PL": "Zmień rozmiar ikon gry", - "pt_BR": "Muda o tamanho do ícone do jogo", - "ru_RU": "Меняет размер обложек", - "sv_SE": "Ändra storleken för spelikonerna", - "th_TH": "เปลี่ยนขนาดของไอคอนเกม", - "tr_TR": "Oyun ikonlarının boyutunu değiştirmeyi sağlar", - "uk_UA": "Змінити розмір обкладинок (значків) ігор", - "zh_CN": "更改游戏图标的显示尺寸", - "zh_TW": "變更遊戲圖示的大小" - } - }, { "ID": "MenuBarOptionsShowConsole", "Translations": { diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml index ca49e8f71..9c5050714 100755 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -81,14 +81,12 @@ Command="{Binding OpenCheatManager}" CommandParameter="{Binding}" Header="{ext:Locale GameListContextMenuManageCheat}" - Icon="{ext:Icon fa-solid fa-code}" - ToolTip.Tip="{ext:Locale GameListContextMenuManageCheatToolTip}" /> + Icon="{ext:Icon fa-solid fa-code}" /> + Icon="{ext:Icon fa-solid fa-sliders}" /> + Icon="{ext:Icon fa-solid fa-circle-info}" /> + Icon="{ext:Icon fa-solid fa-rotate}" /> diff --git a/src/Ryujinx/UI/Views/Main/MainViewControls.axaml b/src/Ryujinx/UI/Views/Main/MainViewControls.axaml index db557b417..282e66cca 100644 --- a/src/Ryujinx/UI/Views/Main/MainViewControls.axaml +++ b/src/Ryujinx/UI/Views/Main/MainViewControls.axaml @@ -49,8 +49,7 @@ + Text="{ext:Locale IconSize}" /> @@ -51,10 +52,8 @@ Date: Sun, 3 Aug 2025 04:39:56 -0500 Subject: [PATCH 03/24] Tooltip Fix Pt.4 (ryubing/ryujinx!101) See merge request ryubing/ryujinx!101 --- assets/locales.json | 300 +++--------------- .../UI/Controls/ApplicationContextMenu.axaml | 6 +- .../UI/Views/Main/MainMenuBarView.axaml | 18 +- 3 files changed, 58 insertions(+), 266 deletions(-) diff --git a/assets/locales.json b/assets/locales.json index 15f4d9ea4..17992c64f 100644 --- a/assets/locales.json +++ b/assets/locales.json @@ -2342,31 +2342,6 @@ "zh_TW": "清除 PPTC" } }, - { - "ID": "GameListContextMenuCacheManagementNukePptcToolTip", - "Translations": { - "ar_SA": "", - "de_DE": "", - "el_GR": "", - "en_US": "Deletes all PPTC cache files for the Application", - "es_ES": "", - "fr_FR": "Supprime tous les fichiers de cache PPTC de l’application", - "he_IL": "", - "it_IT": "", - "ja_JP": "", - "ko_KR": "앱의 모든 PPTC 캐시 파일 삭제", - "no_NO": "Sletter alle PPTC-cache-filer for applikasjonen", - "pl_PL": "", - "pt_BR": "Apaga os arquivos de cache PPTC do aplicativo", - "ru_RU": "Удаляет все файлы кэша PPTC для приложения", - "sv_SE": "Tar bort alla PPTC-cachefiler för applikationen", - "th_TH": "", - "tr_TR": "", - "uk_UA": "Видаляє всі файли кешу PPTC для застосунку", - "zh_CN": "删除应用程序的所有 PPTC 缓存", - "zh_TW": "清除應用程式的 PPTC" - } - }, { "ID": "GameListContextMenuCacheManagementPurgeShaderCache", "Translations": { @@ -2392,31 +2367,6 @@ "zh_TW": "清除著色器快取" } }, - { - "ID": "GameListContextMenuCacheManagementPurgeShaderCacheToolTip", - "Translations": { - "ar_SA": "يحذف ذاكرة مرشحات الفيديو المؤقتة الخاصة بالتطبيق", - "de_DE": "Löscht den Shader-Cache der Anwendung", - "el_GR": "Διαγράφει την προσωρινή μνήμη Shader της εφαρμογής", - "en_US": "Deletes Application's shader cache", - "es_ES": "Eliminar la caché de sombreadores de esta aplicación", - "fr_FR": "Supprime le cache des shaders de l’application", - "he_IL": "מוחק את מטמון ההצללות של היישום", - "it_IT": "Elimina la cache degli shader dell'applicazione", - "ja_JP": "アプリケーションのシェーダーキャッシュを破棄します", - "ko_KR": "앱의 셰이더 캐시 삭제", - "no_NO": "Sletter applikasjonens shader cache", - "pl_PL": "Usuwa pamięć podręczną cieni danej aplikacji", - "pt_BR": "Deleta o cache de Shader do jogo armazenado em disco", - "ru_RU": "Удаляет кеш шейдеров приложения", - "sv_SE": "Tar bort applikationens shader cache", - "th_TH": "ลบแคช แสงเงา ของแอปพลิเคชัน", - "tr_TR": "Uygulamanın shader önbelleğini temizler", - "uk_UA": "Видаляє кеш шейдерів застосунку (гри)", - "zh_CN": "删除游戏的着色器缓存文件,下次启动游戏时重新生成着色器缓存文件", - "zh_TW": "清除應用程式的著色器快取檔案" - } - }, { "ID": "GameListContextMenuCacheManagementOpenPptcDirectory", "Translations": { @@ -2817,56 +2767,6 @@ "zh_TW": "在 macOS 的應用程式資料夾中建立捷徑,啟動選取的應用程式" } }, - { - "ID": "CreateCustomConfigurationToolTip", - "Translations": { - "ar_SA": "ينشئ تكوينًا مستقلًا للعبة الحالية", - "de_DE": "Erstellt eine unabhängige Konfiguration für das aktuelle Spiel", - "el_GR": "Δημιουργεί μια ανεξάρτητη διαμόρφωση για το τρέχον παιχνίδι", - "en_US": "Creates an independent configuration for the selected game", - "es_ES": "Crea una configuración independiente para el juego actual", - "fr_FR": "Crée une configuration indépendante pour le jeu sélectionné", - "he_IL": "יוצר תצורה עצמאית למשחק הנוכחי", - "it_IT": "Crea una configurazione indipendente per il gioco attuale", - "ja_JP": "現在のゲーム用の独立した設定を作成します", - "ko_KR": "현재 게임에 대한 독립적인 설정을 생성합니다", - "no_NO": "Oppretter en uavhengig konfigurasjon for det gjeldende spillet", - "pl_PL": "Tworzy niezależną konfigurację dla bieżącej gry", - "pt_BR": "Cria uma configuração independente para o jogo atual", - "ru_RU": "Создает независимую конфигурацию для текущей игры", - "sv_SE": "Skapar en oberoende konfiguration för det aktuella spelet", - "th_TH": "สร้างการกำหนดค่าที่เป็นอิสระสำหรับเกมปัจจุบัน", - "tr_TR": "Mevcut oyun için bağımsız bir yapılandırma oluşturur", - "uk_UA": "Створюйте незалежну конфігурацію для поточної гри", - "zh_CN": "为当前游戏创建独立的配置", - "zh_TW": "為已選擇的遊戲建立遊戲獨立自訂 (game-specific) 的設定檔" - } - }, - { - "ID": "EditCustomConfigurationToolTip", - "Translations": { - "ar_SA": "", - "de_DE": "", - "el_GR": "", - "en_US": "Edit your existing independent configuration for the selected game", - "es_ES": "", - "fr_FR": "Modifier votre configuration indépendante existante pour le jeu sélectionné", - "he_IL": "", - "it_IT": "", - "ja_JP": "", - "ko_KR": "선택한 게임에 대한 기존 독립 구성 편집", - "no_NO": "Rediger din eksisterende uavhengige konfigurasjon for det valgte spillet", - "pl_PL": "", - "pt_BR": "Editar sua configuração independente existente para o jogo selecionado", - "ru_RU": "Отредактировать существующие независимые параметры для выбранной игры.", - "sv_SE": "Redigera din befintliga oberoende konfiguration för det valda spelet", - "th_TH": "", - "tr_TR": "", - "uk_UA": "Відредагувати наявну індивідуальну конфігурацію для цієї гри.", - "zh_CN": "编辑选定游戏的现存独立配置", - "zh_TW": "為已選擇的遊戲編輯遊戲獨立自訂 (game-specific) 的設定檔" - } - }, { "ID": "GameListContextMenuShowCompatEntry", "Translations": { @@ -2917,6 +2817,56 @@ "zh_TW": "在相容性列表中顯示已選擇的遊戲。你也可以透過「說明」選單開啟。" } }, + { + "ID": "CreateCustomConfigurationToolTip", + "Translations": { + "ar_SA": "ينشئ تكوينًا مستقلًا للعبة الحالية", + "de_DE": "Erstellt eine unabhängige Konfiguration für das aktuelle Spiel", + "el_GR": "Δημιουργεί μια ανεξάρτητη διαμόρφωση για το τρέχον παιχνίδι", + "en_US": "Creates an independent configuration for the selected game", + "es_ES": "Crea una configuración independiente para el juego actual", + "fr_FR": "Crée une configuration indépendante pour le jeu sélectionné", + "he_IL": "יוצר תצורה עצמאית למשחק הנוכחי", + "it_IT": "Crea una configurazione indipendente per il gioco attuale", + "ja_JP": "現在のゲーム用の独立した設定を作成します", + "ko_KR": "현재 게임에 대한 독립적인 설정을 생성합니다", + "no_NO": "Oppretter en uavhengig konfigurasjon for det gjeldende spillet", + "pl_PL": "Tworzy niezależną konfigurację dla bieżącej gry", + "pt_BR": "Cria uma configuração independente para o jogo atual", + "ru_RU": "Создает независимую конфигурацию для текущей игры", + "sv_SE": "Skapar en oberoende konfiguration för det aktuella spelet", + "th_TH": "สร้างการกำหนดค่าที่เป็นอิสระสำหรับเกมปัจจุบัน", + "tr_TR": "Mevcut oyun için bağımsız bir yapılandırma oluşturur", + "uk_UA": "Створюйте незалежну конфігурацію для поточної гри", + "zh_CN": "为当前游戏创建独立的配置", + "zh_TW": "為已選擇的遊戲建立遊戲獨立自訂 (game-specific) 的設定檔" + } + }, + { + "ID": "EditCustomConfigurationToolTip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Edit your existing independent configuration for the selected game", + "es_ES": "", + "fr_FR": "Modifier votre configuration indépendante existante pour le jeu sélectionné", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "선택한 게임에 대한 기존 독립 구성 편집", + "no_NO": "Rediger din eksisterende uavhengige konfigurasjon for det valgte spillet", + "pl_PL": "", + "pt_BR": "Editar sua configuração independente existente para o jogo selecionado", + "ru_RU": "Отредактировать существующие независимые параметры для выбранной игры.", + "sv_SE": "Redigera din befintliga oberoende konfiguration för det valda spelet", + "th_TH": "", + "tr_TR": "", + "uk_UA": "Відредагувати наявну індивідуальну конфігурацію для цієї гри.", + "zh_CN": "编辑选定游戏的现存独立配置", + "zh_TW": "為已選擇的遊戲編輯遊戲獨立自訂 (game-specific) 的設定檔" + } + }, { "ID": "GameListContextMenuShowGameData", "Translations": { @@ -17292,156 +17242,6 @@ "zh_TW": "在控制台中輸出偵錯日誌訊息。\n\n只有在人員特別指示的情況下才能使用,因為這會導致日誌難以閱讀,並降低模擬器效能。" } }, - { - "ID": "LoadApplicationFileTooltip", - "Translations": { - "ar_SA": "افتح مستكشف الملفات لاختيار ملف متوافق مع سويتش لتحميله", - "de_DE": "Öffnet die Dateiauswahl um Datei zu laden, welche mit der Switch kompatibel ist", - "el_GR": "Ανοίξτε έναν επιλογέα αρχείων για να επιλέξετε ένα αρχείο συμβατό με το Switch για φόρτωση", - "en_US": "Open a file explorer to choose a Switch compatible file to load", - "es_ES": "Abre el explorador de archivos para elegir un archivo compatible con Switch para cargar", - "fr_FR": "Ouvre un explorateur de fichiers pour choisir un fichier compatible Switch à charger.", - "he_IL": "פתח סייר קבצים כדי לבחור קובץ תואם סוויץ' לטעינה", - "it_IT": "Apri un selettore file per scegliere un file compatibile con Switch da caricare", - "ja_JP": "ロードする Switch 互換のファイルを選択するためファイルエクスプローラを開きます", - "ko_KR": "파일 탐색기를 열어 불러올 Switch 호환 파일을 선택", - "no_NO": "Åpne filutforsker for å velge en Switch kompatibel fil å laste", - "pl_PL": "Otwórz eksplorator plików, aby wybrać plik kompatybilny z Switch do wczytania", - "pt_BR": "Abre um explorador de arquivos para escolher um arquivo compatível com o Switch para carregar", - "ru_RU": "Открывает файловый менеджер для выбора файла, совместимого с Nintendo Switch.", - "sv_SE": "Öppna en filutforskare för att välja en Switch-kompatibel fil att läsa in", - "th_TH": "เปิดตัวสำรวจไฟล์เพื่อเลือกไฟล์ที่เข้ากันได้กับ Switch ที่จะโหลด", - "tr_TR": "Switch ile uyumlu bir dosya yüklemek için dosya tarayıcısını açar", - "uk_UA": "Відкриває Файловий провідник, щоб обрати для завантаження сумісний зі Switch файл", - "zh_CN": "选择 Switch 游戏文件并加载", - "zh_TW": "開啟檔案總管,選擇與 Switch 相容的檔案來載入" - } - }, - { - "ID": "LoadApplicationFolderTooltip", - "Translations": { - "ar_SA": "افتح مستكشف الملفات لاختيار تطبيق متوافق مع سويتش للتحميل", - "de_DE": "Öffnet die Dateiauswahl um ein Spiel zu laden, welches mit der Switch kompatibel ist", - "el_GR": "Ανοίξτε έναν επιλογέα αρχείων για να επιλέξετε μία μη συσκευασμένη εφαρμογή, συμβατή με το Switch για φόρτωση", - "en_US": "Open a file explorer to choose a Switch compatible, unpacked application to load", - "es_ES": "Abre el explorador de archivos para elegir un archivo desempaquetado y compatible con Switch para cargar", - "fr_FR": "Ouvre un explorateur de fichiers pour choisir une application dépaquetée compatible Switch à charger.", - "he_IL": "פתח סייר קבצים כדי לבחור יישום תואם סוויץ', לא ארוז לטעינה.", - "it_IT": "Apri un selettore file per scegliere un'applicazione estratta compatibile con Switch da caricare", - "ja_JP": "ロードする Switch 互換の展開済みアプリケーションを選択するためファイルエクスプローラを開きます", - "ko_KR": "Switch와 호환되는 압축 해제된 앱을 선택하여 불러오려면 파일 탐색기를 엽니다.", - "no_NO": "Åpne en filutforsker for å velge en Switch kompatibel, upakket applikasjon for å laste", - "pl_PL": "Otwórz eksplorator plików, aby wybrać zgodną z Switch, rozpakowaną aplikację do załadowania", - "pt_BR": "Abre um explorador de arquivos para escolher um aplicativo descompactado compatível com o Switch para carregar", - "ru_RU": "Открывает файловый менеджер для выбора распакованного приложения, совместимого с Nintendo Switch.", - "sv_SE": "Öppna en filutforskare för att välja en Switch-kompatibel, uppackad applikation att läsa in", - "th_TH": "เปิดตัวสำรวจไฟล์เพื่อเลือกไฟล์ที่เข้ากันได้กับ Switch ที่จะโหลด", - "tr_TR": "Switch ile uyumlu ayrıştırılmamış bir uygulama yüklemek için dosya tarayıcısını açar", - "uk_UA": "Відкриває Файловий провідник, щоб обрати сумісну зі Switch розпаковану програму для завантаження", - "zh_CN": "选择解包后的 Switch 游戏目录并加载", - "zh_TW": "開啟檔案總管,選擇與 Switch 相容且未封裝的應用程式來載入" - } - }, - { - "ID": "LoadDlcFromFolderTooltip", - "Translations": { - "ar_SA": "", - "de_DE": "", - "el_GR": "", - "en_US": "Open a file explorer to choose one or more folders to bulk load DLC from", - "es_ES": "Abrir un explorador de archivos para seleccionar una o más carpetas para cargar DLC de forma masiva", - "fr_FR": "Ouvre un explorateur de fichiers pour choisir un ou plusieurs dossiers afin de charger en masse des DLC", - "he_IL": "", - "it_IT": "Apri un selettore file per scegliere una o più cartelle dalle quali caricare DLC in blocco", - "ja_JP": "", - "ko_KR": "파일 탐색기를 열어 DLC를 일괄 불러오기할 폴더를 하나 이상 선택", - "no_NO": "Åpne en filutforsker for å velge en eller flere mapper å laste inn DLC fra", - "pl_PL": "", - "pt_BR": "Abre um explorador de arquivos para escolher uma ou mais pastas para carregar DLC em massa", - "ru_RU": "Открывает проводник, для выбора одной или нескольких папок для массовой загрузки DLC", - "sv_SE": "Öppna en filutforskare för att välja en eller flera mappar att läsa in alla DLC från", - "th_TH": "เปิดตัวสำรวจไฟล์เพื่อเลือกหนึ่งโฟลเดอร์ขึ้นไปเพื่อโหลด DLC จำนวนมาก", - "tr_TR": "", - "uk_UA": "Відкриває Файловий провідник для обрання однієї або декількох тек для масового завантаження DLC", - "zh_CN": "打开文件资源管理器以选择一个或多个文件夹来批量加载 DLC。", - "zh_TW": "開啟檔案總管,選擇一個或多個資料夾來大量載入 DLC" - } - }, - { - "ID": "LoadTitleUpdatesFromFolderTooltip", - "Translations": { - "ar_SA": "", - "de_DE": "", - "el_GR": "", - "en_US": "Open a file explorer to choose one or more folders to bulk load title updates from", - "es_ES": "Abrir un explorador de archivos para seleccionar una o más carpetas para cargar actualizaciones de título de forma masiva", - "fr_FR": "Ouvre un explorateur de fichiers pour choisir un ou plusieurs dossiers afin de charger en masse des mises à jour de titre.", - "he_IL": "", - "it_IT": "Apri un selettore file per scegliere una o più cartelle dalle quali caricare aggiornamenti in blocco", - "ja_JP": "", - "ko_KR": "파일 탐색기를 열어 하나 이상의 폴더를 선택하여 대량으로 타이틀 업데이트 불러오기", - "no_NO": "Åpne en filutforsker for å velge en eller flere mapper som du vil laste inn titteloppdateringer fra", - "pl_PL": "", - "pt_BR": "Abre um explorador de arquivos para escolher uma ou mais pastas para carregar em massa as atualizações de títulos", - "ru_RU": "Открывает проводник, чтобы выбрать одну или несколько папок для массовой загрузки обновлений приложений", - "sv_SE": "Öppna en filutforskare för att välja en eller flera mappar att läsa in alla titeluppdateringar från", - "th_TH": "เปิดตัวสำรวจไฟล์เพื่อเลือกหนึ่งโฟลเดอร์ขึ้นไปเพื่อโหลดไฟล์อัปเดตจำนวนมาก", - "tr_TR": "", - "uk_UA": "Відкриває Файловий Провідник для обрання однієї або декількох тек для масового завантаження оновлень", - "zh_CN": "打开文件资源管理器以选择一个或多个文件夹来批量加载游戏更新。", - "zh_TW": "開啟檔案總管,選擇一個或多個資料夾來大量載入遊戲更新" - } - }, - { - "ID": "ExitTooltip", - "Translations": { - "ar_SA": "الخروج من ريوجينكس", - "de_DE": "Beendet Ryujinx", - "el_GR": "Έξοδος από το Ryujinx", - "en_US": "Exit Ryujinx", - "es_ES": "Cierra Ryujinx", - "fr_FR": "Quitter Ryujinx", - "he_IL": "צא מריוג'ינקס", - "it_IT": "Esci da Ryujinx", - "ja_JP": "Ryujinx を終了します", - "ko_KR": "Ryujinx 종료", - "no_NO": "Avslutt Ryujinx", - "pl_PL": "Wyjdź z Ryujinx", - "pt_BR": "Sair do Ryujinx", - "ru_RU": "Выйти из Ryujinx", - "sv_SE": "Avsluta Ryujinx", - "th_TH": "ออกจากโปรแกรม Ryujinx", - "tr_TR": "Ryujinx'ten çıkış yapmayı sağlar", - "uk_UA": "Закриває Ryujinx", - "zh_CN": "退出 Ryujinx 模拟器", - "zh_TW": "結束 Ryujinx" - } - }, - { - "ID": "StopEmulationTooltip", - "Translations": { - "ar_SA": "إيقاف محاكاة اللعبة الحالية والعودة إلى اختيار اللعبة", - "de_DE": "Beendet die Emulation des derzeitigen Spiels und kehrt zu der Spielauswahl zurück", - "el_GR": "Σταματήστε την εξομοίωση του τρέχοντος παιχνιδιού και επιστρέψτε στην επιλογή παιχνιδιού", - "en_US": "Stop emulation of the current game and return to game selection", - "es_ES": "Detiene la emulación del juego actual y regresa a la selección de juegos", - "fr_FR": "Arrêter l'émulation du jeu en cours et revenir à la sélection des jeux", - "he_IL": "הפסק את הדמייה של המשחק הנוכחי וחזור למסך בחירת המשחק", - "it_IT": "Ferma l'emulazione del gioco attuale e torna alla selezione dei giochi", - "ja_JP": "ゲームのエミュレーションを中止してゲーム選択画面に戻ります", - "ko_KR": "현재 게임의 에뮬레이션을 중지하고 게임 선택으로 돌아가기", - "no_NO": "Stopp emuleringen av dette spillet og gå tilbake til spill valg", - "pl_PL": "Zatrzymaj emulację bieżącej gry i wróć do wyboru gier", - "pt_BR": "Parar emulação do jogo atual e voltar a seleção de jogos", - "ru_RU": "Остановка эмуляции текущей игры с последующим возвратом к списку игр", - "sv_SE": "Stoppa emulering av aktuellt spel och återgå till spelväljaren", - "th_TH": "หยุดการจำลองของเกมที่เปิดอยู่ในปัจจุบันและกลับไปยังการเลือกเกม", - "tr_TR": "Oynanmakta olan oyunun emülasyonunu durdurup oyun seçimine geri döndürür", - "uk_UA": "Зупиняє емуляцію поточної гри та повертається до вибору гри", - "zh_CN": "停止运行当前游戏,并回到主界面", - "zh_TW": "停止模擬目前遊戲,返回遊戲選擇介面" - } - }, { "ID": "GridSize", "Translations": { diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml index 9c5050714..cb71ffcd2 100755 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -118,14 +118,12 @@ Command="{Binding NukePtcCache}" CommandParameter="{Binding}" Header="{ext:Locale GameListContextMenuCacheManagementNukePptc}" - Icon="{ext:Icon fa-solid fa-trash-can}" - ToolTip.Tip="{ext:Locale GameListContextMenuCacheManagementNukePptcToolTip}" /> + Icon="{ext:Icon fa-solid fa-trash-can}" /> + Icon="{ext:Icon fa-solid fa-trash-can}" /> + IsEnabled="{Binding EnableNonGameRunningControls}" /> + IsEnabled="{Binding EnableNonGameRunningControls}" /> + IsEnabled="{Binding EnableNonGameRunningControls}" /> + IsEnabled="{Binding EnableNonGameRunningControls}" /> + Icon="{ext:Icon fa-solid fa-power-off}" /> + IsEnabled="{Binding IsGameRunning}" /> Date: Mon, 4 Aug 2025 20:45:15 -0500 Subject: [PATCH 04/24] Add GDB Stub (ryubing/ryujinx!71) See merge request ryubing/ryujinx!71 --- assets/locales.json | 200 +++ .../Instructions/NativeInterface.cs | 7 +- src/ARMeilleure/Optimizations.cs | 1 + src/ARMeilleure/State/ExecutionContext.cs | 38 +- src/ARMeilleure/State/NativeContext.cs | 16 + src/ARMeilleure/Translation/PTC/Ptc.cs | 13 +- src/ARMeilleure/Translation/Translator.cs | 149 +- src/Ryujinx.Common/Logging/LogClass.cs | 1 + src/Ryujinx.Cpu/AppleHv/Arm/ExceptionLevel.cs | 10 + src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs | 81 +- .../AppleHv/HvExecutionContextShadow.cs | 11 + .../AppleHv/HvExecutionContextVcpu.cs | 16 +- .../AppleHv/IHvExecutionContext.cs | 4 + src/Ryujinx.Cpu/ExceptionCallbacks.cs | 8 + src/Ryujinx.Cpu/IExecutionContext.cs | 24 + src/Ryujinx.Cpu/Jit/JitExecutionContext.cs | 24 + .../LightningJit/State/ExecutionContext.cs | 20 + src/Ryujinx.HLE/Debugger/BreakpointManager.cs | 203 +++ src/Ryujinx.HLE/Debugger/DebugState.cs | 9 + src/Ryujinx.HLE/Debugger/Debugger.cs | 1327 +++++++++++++++++ src/Ryujinx.HLE/Debugger/GdbSignal.cs | 15 + .../Debugger/GdbXml/aarch64-core.xml | 93 ++ .../Debugger/GdbXml/aarch64-fpu.xml | 159 ++ src/Ryujinx.HLE/Debugger/GdbXml/arm-core.xml | 27 + src/Ryujinx.HLE/Debugger/GdbXml/arm-neon.xml | 86 ++ src/Ryujinx.HLE/Debugger/GdbXml/target32.xml | 14 + src/Ryujinx.HLE/Debugger/GdbXml/target64.xml | 14 + .../Debugger/IDebuggableProcess.cs | 21 + .../Debugger/Message/BreakInMessage.cs | 6 + .../Debugger/Message/CommandMessage.cs | 12 + src/Ryujinx.HLE/Debugger/Message/IMessage.cs | 6 + .../Debugger/Message/KillMessage.cs | 6 + .../Debugger/Message/SendNackMessage.cs | 6 + .../Debugger/Message/ThreadBreakMessage.cs | 18 + .../Debugger/RegisterInformation.cs | 28 + src/Ryujinx.HLE/Debugger/StringStream.cs | 109 ++ .../HOS/ArmProcessContextFactory.cs | 2 +- src/Ryujinx.HLE/HOS/Horizon.cs | 17 + .../HOS/Kernel/Process/HleProcessDebugger.cs | 24 +- .../HOS/Kernel/Process/KProcess.cs | 206 ++- .../Kernel/Process/ProcessExecutionContext.cs | 9 + .../HOS/Kernel/Threading/KScheduler.cs | 1 + .../HOS/Kernel/Threading/KThread.cs | 110 +- src/Ryujinx.HLE/HleConfiguration.cs | 21 + src/Ryujinx.HLE/Ryujinx.HLE.csproj | 12 + src/Ryujinx.HLE/Switch.cs | 4 + src/Ryujinx/Headless/HeadlessRyujinx.Init.cs | 3 + src/Ryujinx/Headless/Options.cs | 11 + src/Ryujinx/Systems/AppHost.cs | 19 + .../Configuration/ConfigurationFileFormat.cs | 15 + .../ConfigurationState.Migration.cs | 4 + .../Configuration/ConfigurationState.Model.cs | 40 + .../Configuration/ConfigurationState.cs | 6 + .../UI/ViewModels/SettingsViewModel.cs | 46 + .../UI/Views/Settings/SettingsDebugView.axaml | 66 + .../Views/Settings/SettingsDebugView.axaml.cs | 13 + src/Ryujinx/UI/Windows/SettingsWindow.axaml | 5 + .../UI/Windows/SettingsWindow.axaml.cs | 3 + 58 files changed, 3394 insertions(+), 25 deletions(-) create mode 100644 src/Ryujinx.Cpu/AppleHv/Arm/ExceptionLevel.cs create mode 100644 src/Ryujinx.HLE/Debugger/BreakpointManager.cs create mode 100644 src/Ryujinx.HLE/Debugger/DebugState.cs create mode 100644 src/Ryujinx.HLE/Debugger/Debugger.cs create mode 100644 src/Ryujinx.HLE/Debugger/GdbSignal.cs create mode 100644 src/Ryujinx.HLE/Debugger/GdbXml/aarch64-core.xml create mode 100644 src/Ryujinx.HLE/Debugger/GdbXml/aarch64-fpu.xml create mode 100644 src/Ryujinx.HLE/Debugger/GdbXml/arm-core.xml create mode 100644 src/Ryujinx.HLE/Debugger/GdbXml/arm-neon.xml create mode 100644 src/Ryujinx.HLE/Debugger/GdbXml/target32.xml create mode 100644 src/Ryujinx.HLE/Debugger/GdbXml/target64.xml create mode 100644 src/Ryujinx.HLE/Debugger/IDebuggableProcess.cs create mode 100644 src/Ryujinx.HLE/Debugger/Message/BreakInMessage.cs create mode 100644 src/Ryujinx.HLE/Debugger/Message/CommandMessage.cs create mode 100644 src/Ryujinx.HLE/Debugger/Message/IMessage.cs create mode 100644 src/Ryujinx.HLE/Debugger/Message/KillMessage.cs create mode 100644 src/Ryujinx.HLE/Debugger/Message/SendNackMessage.cs create mode 100644 src/Ryujinx.HLE/Debugger/Message/ThreadBreakMessage.cs create mode 100644 src/Ryujinx.HLE/Debugger/RegisterInformation.cs create mode 100644 src/Ryujinx.HLE/Debugger/StringStream.cs create mode 100644 src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml create mode 100644 src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml.cs diff --git a/assets/locales.json b/assets/locales.json index 17992c64f..9a1361218 100644 --- a/assets/locales.json +++ b/assets/locales.json @@ -24166,6 +24166,206 @@ "zh_CN": "动态 Rich Presence", "zh_TW": "動態 Rich Presence" } + }, + { + "ID": "SettingsTabDebug", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Debug", + "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": "SettingsTabDebugTitle", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Debug", + "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": "SettingsTabDebugNote", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "WARNING: For developer use only, will reduce performance", + "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": "SettingsTabDebugEnableGDBStub", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Enable GDB Stub", + "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": "SettingsTabDebugGDBStubToggleTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Enables the GDB stub which makes it possible to debug the running application. For development use only!", + "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": "SettingsTabDebugGDBStubPort", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "GDB Stub Port:", + "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": "SettingsTabDebugSuspendOnStart", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Suspend Application on Start", + "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": "SettingsTabDebugSuspendOnStartTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Suspends the application before executing the first instruction, allowing for debugging from the earliest point.", + "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": "" + } } ] } \ No newline at end of file diff --git a/src/ARMeilleure/Instructions/NativeInterface.cs b/src/ARMeilleure/Instructions/NativeInterface.cs index b4922629b..d43e20d83 100644 --- a/src/ARMeilleure/Instructions/NativeInterface.cs +++ b/src/ARMeilleure/Instructions/NativeInterface.cs @@ -3,6 +3,7 @@ using ARMeilleure.State; using ARMeilleure.Translation; using System; using System.Runtime.InteropServices; +using ExecutionContext = ARMeilleure.State.ExecutionContext; namespace ARMeilleure.Instructions { @@ -200,7 +201,11 @@ namespace ARMeilleure.Instructions ExecutionContext context = GetContext(); - context.CheckInterrupt(); + // If debugging, we'll handle interrupts outside + if (!Optimizations.EnableDebugging) + { + context.CheckInterrupt(); + } Statistics.ResumeTimer(); diff --git a/src/ARMeilleure/Optimizations.cs b/src/ARMeilleure/Optimizations.cs index 18390de31..6dd7befe7 100644 --- a/src/ARMeilleure/Optimizations.cs +++ b/src/ARMeilleure/Optimizations.cs @@ -12,6 +12,7 @@ namespace ARMeilleure public static bool AllowLcqInFunctionTable { get; set; } = true; public static bool UseUnmanagedDispatchLoop { get; set; } = true; + public static bool EnableDebugging { get; set; } = false; public static bool UseAdvSimdIfAvailable { get; set; } = true; public static bool UseArm64AesIfAvailable { get; set; } = true; diff --git a/src/ARMeilleure/State/ExecutionContext.cs b/src/ARMeilleure/State/ExecutionContext.cs index 223e59d79..fa1a4a032 100644 --- a/src/ARMeilleure/State/ExecutionContext.cs +++ b/src/ARMeilleure/State/ExecutionContext.cs @@ -1,4 +1,5 @@ using ARMeilleure.Memory; +using System.Threading; namespace ARMeilleure.State { @@ -10,7 +11,7 @@ namespace ARMeilleure.State internal nint NativeContextPtr => _nativeContext.BasePtr; - private bool _interrupted; + internal bool Interrupted { get; private set; } private readonly ICounter _counter; @@ -65,6 +66,8 @@ namespace ARMeilleure.State public bool IsAarch32 { get; set; } + public ulong ThreadUid { get; set; } + internal ExecutionMode ExecutionMode { get @@ -90,14 +93,19 @@ namespace ARMeilleure.State private readonly ExceptionCallbackNoArgs _interruptCallback; private readonly ExceptionCallback _breakCallback; + private readonly ExceptionCallbackNoArgs _stepCallback; private readonly ExceptionCallback _supervisorCallback; private readonly ExceptionCallback _undefinedCallback; + internal int ShouldStep; + public ulong DebugPc { get; set; } + public ExecutionContext( IJitMemoryAllocator allocator, ICounter counter, ExceptionCallbackNoArgs interruptCallback = null, ExceptionCallback breakCallback = null, + ExceptionCallbackNoArgs stepCallback = null, ExceptionCallback supervisorCallback = null, ExceptionCallback undefinedCallback = null) { @@ -105,6 +113,7 @@ namespace ARMeilleure.State _counter = counter; _interruptCallback = interruptCallback; _breakCallback = breakCallback; + _stepCallback = stepCallback; _supervisorCallback = supervisorCallback; _undefinedCallback = undefinedCallback; @@ -127,9 +136,9 @@ namespace ARMeilleure.State internal void CheckInterrupt() { - if (_interrupted) + if (Interrupted) { - _interrupted = false; + Interrupted = false; _interruptCallback?.Invoke(this); } @@ -139,16 +148,37 @@ namespace ARMeilleure.State public void RequestInterrupt() { - _interrupted = true; + Interrupted = true; + } + + public void StepHandler() + { + _stepCallback?.Invoke(this); + } + + public void RequestDebugStep() + { + Interlocked.Exchange(ref ShouldStep, 1); + RequestInterrupt(); } internal void OnBreak(ulong address, int imm) { + if (Optimizations.EnableDebugging) + { + DebugPc = Pc; + } + _breakCallback?.Invoke(this, address, imm); } internal void OnSupervisorCall(ulong address, int imm) { + if (Optimizations.EnableDebugging) + { + DebugPc = Pc; + } + _supervisorCallback?.Invoke(this, address, imm); } diff --git a/src/ARMeilleure/State/NativeContext.cs b/src/ARMeilleure/State/NativeContext.cs index c90e522a9..a9f1c3dab 100644 --- a/src/ARMeilleure/State/NativeContext.cs +++ b/src/ARMeilleure/State/NativeContext.cs @@ -22,6 +22,12 @@ namespace ARMeilleure.State public ulong ExclusiveValueHigh; public int Running; public long Tpidr2El0; + + /// + /// Precise PC value used for debugging. + /// This will only be set when Optimizations.EnableDebugging is true. + /// + public ulong DebugPrecisePc; } private static NativeCtxStorage _dummyStorage = new(); @@ -39,6 +45,11 @@ namespace ARMeilleure.State public ulong GetPc() { + if (Optimizations.EnableDebugging) + { + return GetStorage().DebugPrecisePc; + } + // TODO: More precise tracking of PC value. return GetStorage().DispatchAddress; } @@ -268,6 +279,11 @@ namespace ARMeilleure.State return StorageOffset(ref _dummyStorage, ref _dummyStorage.Running); } + public static int GetDebugPrecisePcOffset() + { + return StorageOffset(ref _dummyStorage, ref _dummyStorage.DebugPrecisePc); + } + private static int StorageOffset(ref NativeCtxStorage storage, ref T target) { return (int)Unsafe.ByteOffset(ref Unsafe.As(ref storage), ref target); diff --git a/src/ARMeilleure/Translation/PTC/Ptc.cs b/src/ARMeilleure/Translation/PTC/Ptc.cs index f36d4256d..c69ebcadb 100644 --- a/src/ARMeilleure/Translation/PTC/Ptc.cs +++ b/src/ARMeilleure/Translation/PTC/Ptc.cs @@ -33,7 +33,7 @@ namespace ARMeilleure.Translation.PTC private const string OuterHeaderMagicString = "PTCohd\0\0"; private const string InnerHeaderMagicString = "PTCihd\0\0"; - private const uint InternalVersion = 7008; //! To be incremented manually for each change to the ARMeilleure project. + private const uint InternalVersion = 7009; //! To be incremented manually for each change to the ARMeilleure project. private const string ActualDir = "0"; private const string BackupDir = "1"; @@ -303,6 +303,13 @@ namespace ARMeilleure.Translation.PTC return false; } + if (outerHeader.DebuggerMode != Optimizations.EnableDebugging) + { + InvalidateCompressedStream(compressedStream); + + return false; + } + nint intPtr = nint.Zero; try @@ -479,6 +486,7 @@ namespace ARMeilleure.Translation.PTC MemoryManagerMode = GetMemoryManagerMode(), OSPlatform = GetOSPlatform(), Architecture = (uint)RuntimeInformation.ProcessArchitecture, + DebuggerMode = Optimizations.EnableDebugging, UncompressedStreamSize = (long)Unsafe.SizeOf() + @@ -1068,7 +1076,7 @@ namespace ARMeilleure.Translation.PTC return osPlatform; } - [StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 86*/)] + [StructLayout(LayoutKind.Sequential, Pack = 1/*, Size = 87*/)] private struct OuterHeader { public ulong Magic; @@ -1080,6 +1088,7 @@ namespace ARMeilleure.Translation.PTC public byte MemoryManagerMode; public uint OSPlatform; public uint Architecture; + public bool DebuggerMode; public long UncompressedStreamSize; diff --git a/src/ARMeilleure/Translation/Translator.cs b/src/ARMeilleure/Translation/Translator.cs index d8528cfd6..bbe641101 100644 --- a/src/ARMeilleure/Translation/Translator.cs +++ b/src/ARMeilleure/Translation/Translator.cs @@ -119,7 +119,25 @@ namespace ARMeilleure.Translation NativeInterface.RegisterThread(context, Memory, this); - if (Optimizations.UseUnmanagedDispatchLoop) + if (Optimizations.EnableDebugging) + { + context.DebugPc = address; + do + { + if (Interlocked.CompareExchange(ref context.ShouldStep, 0, 1) == 1) + { + context.DebugPc = Step(context, context.DebugPc); + context.StepHandler(); + } + else + { + context.DebugPc = ExecuteSingle(context, context.DebugPc); + } + context.CheckInterrupt(); + } + while (context.Running && context.DebugPc != 0); + } + else if (Optimizations.UseUnmanagedDispatchLoop) { Stubs.DispatchLoop(context.NativeContextPtr, address); } @@ -175,8 +193,24 @@ namespace ARMeilleure.Translation return nextAddr; } - public ulong Step(State.ExecutionContext context, ulong address) + private ulong Step(State.ExecutionContext context, ulong address) { + try + { + OpCode opCode = Decoder.DecodeOpCode(Memory, address, context.ExecutionMode); + + // For branch instructions during single-stepping, we handle them manually + // func.Execute() will sometimes execute the entire function call, which is not what we want + if (opCode.Instruction.Name is InstName.Bl or InstName.Blr or InstName.Blx or InstName.Br) + { + return ExecuteBranchInstructionForStepping(context, address, opCode); + } + } + catch + { + // ignore + } + TranslatedFunction func = Translate(address, context.ExecutionMode, highCq: false, singleStep: true); address = func.Execute(Stubs.ContextWrapper, context); @@ -186,6 +220,94 @@ namespace ARMeilleure.Translation return address; } + private static ulong ExecuteBranchInstructionForStepping(State.ExecutionContext context, ulong address, OpCode opCode) + { + switch (opCode.Instruction.Name) + { + case InstName.Bl: + if (opCode is IOpCodeBImm opBImm) + { + // Set link register + if (context.ExecutionMode == ExecutionMode.Aarch64) + { + context.SetX(30, address + (ulong)opCode.OpCodeSizeInBytes); // LR = X30 + } + else + { + // For ARM32, need to set the appropriate return address + uint returnAddr = opCode is OpCode32 op32 && op32.IsThumb + ? (uint)address + (uint)opCode.OpCodeSizeInBytes | 1u // Thumb bit set + : (uint)address + (uint)opCode.OpCodeSizeInBytes; + context.SetX(14, returnAddr); // LR = R14 + } + return (ulong)opBImm.Immediate; + } + break; + + case InstName.Blr: + if (opCode is OpCodeBReg opBReg) + { + // Set link register + if (context.ExecutionMode == ExecutionMode.Aarch64) + { + context.SetX(30, address + (ulong)opCode.OpCodeSizeInBytes); // LR = X30 + } + else + { + uint returnAddr = opCode is OpCode32 op32 && op32.IsThumb + ? (uint)address + (uint)opCode.OpCodeSizeInBytes | 1u // Thumb bit set + : (uint)address + (uint)opCode.OpCodeSizeInBytes; + context.SetX(14, returnAddr); // LR = R14 + } + return context.GetX(opBReg.Rn); + } + break; + + case InstName.Blx: + if (opCode is IOpCodeBImm opBlxImm) + { + // Handle mode switching for BLX + if (opCode is OpCode32 op32) + { + uint returnAddr = op32.IsThumb + ? (uint)address + (uint)opCode.OpCodeSizeInBytes | 1u + : (uint)address + (uint)opCode.OpCodeSizeInBytes; + context.SetX(14, returnAddr); + + // BLX switches between ARM and Thumb modes + context.SetPstateFlag(PState.TFlag, !op32.IsThumb); + } + return (ulong)opBlxImm.Immediate; + } + else if (opCode is IOpCode32BReg opBlxReg) + { + if (opCode is OpCode32 op32) + { + uint returnAddr = op32.IsThumb + ? (uint)address + (uint)opCode.OpCodeSizeInBytes | 1u + : (uint)address + (uint)opCode.OpCodeSizeInBytes; + context.SetX(14, returnAddr); + + // For BLX register, the target address determines the mode + ulong targetAddr = context.GetX(opBlxReg.Rm); + context.SetPstateFlag(PState.TFlag, (targetAddr & 1) != 0); + return targetAddr & ~1UL; // Clear the Thumb bit for the actual address + } + } + break; + + case InstName.Br: + if (opCode is OpCodeBReg opBr) + { + // BR doesn't set link register, just branches to the target + return context.GetX(opBr.Rn); + } + break; + } + + throw new InvalidOperationException($"Unhandled branch instruction: {opCode.Instruction.Name}"); + } + internal TranslatedFunction GetOrTranslate(ulong address, ExecutionMode mode) { if (!Functions.TryGetValue(address, out TranslatedFunction func)) @@ -367,9 +489,13 @@ namespace ARMeilleure.Translation if (block.Exit) { - // Left option here as it may be useful if we need to return to managed rather than tail call in - // future. (eg. for debug) - bool useReturns = false; + // Return to managed rather than tail call. + bool useReturns = Optimizations.EnableDebugging; + + if (Optimizations.EnableDebugging) + { + EmitDebugPrecisePcUpdate(context, block.Address); + } InstEmitFlowHelper.EmitVirtualJump(context, Const(block.Address), isReturn: useReturns); } @@ -393,6 +519,11 @@ namespace ARMeilleure.Translation } } + if (Optimizations.EnableDebugging) + { + EmitDebugPrecisePcUpdate(context, opCode.Address); + } + Operand lblPredicateSkip = default; if (context.IsInIfThenBlock && context.CurrentIfThenBlockCond != Condition.Al) @@ -489,6 +620,14 @@ namespace ARMeilleure.Translation context.MarkLabel(lblExit); } + internal static void EmitDebugPrecisePcUpdate(EmitterContext context, ulong address) + { + long debugPrecisePcOffs = NativeContext.GetDebugPrecisePcOffset(); + + Operand debugPrecisePcAddr = context.Add(context.LoadArgument(OperandType.I64, 0), Const(debugPrecisePcOffs)); + context.Store(debugPrecisePcAddr, Const(address)); + } + public void InvalidateJitCacheRegion(ulong address, ulong size) { ulong[] overlapAddresses = []; diff --git a/src/Ryujinx.Common/Logging/LogClass.cs b/src/Ryujinx.Common/Logging/LogClass.cs index a4117580e..89f0336dc 100644 --- a/src/Ryujinx.Common/Logging/LogClass.cs +++ b/src/Ryujinx.Common/Logging/LogClass.cs @@ -13,6 +13,7 @@ namespace Ryujinx.Common.Logging Cpu, Emulation, FFmpeg, + GdbStub, Font, Gpu, Hid, diff --git a/src/Ryujinx.Cpu/AppleHv/Arm/ExceptionLevel.cs b/src/Ryujinx.Cpu/AppleHv/Arm/ExceptionLevel.cs new file mode 100644 index 000000000..08114e12a --- /dev/null +++ b/src/Ryujinx.Cpu/AppleHv/Arm/ExceptionLevel.cs @@ -0,0 +1,10 @@ +namespace Ryujinx.Cpu.AppleHv.Arm +{ + enum ExceptionLevel : uint + { + PstateMask = 0xfffffff0, + EL1h = 0b0101, + El1t = 0b0100, + EL0 = 0b0000, + } +} diff --git a/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs b/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs index 53cea5385..f13662e44 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvExecutionContext.cs @@ -11,7 +11,18 @@ namespace Ryujinx.Cpu.AppleHv class HvExecutionContext : IExecutionContext { /// - public ulong Pc => _impl.ElrEl1; + public ulong Pc + { + get + { + uint currentEl = Pstate & ~((uint)ExceptionLevel.PstateMask); + if (currentEl == (uint)ExceptionLevel.EL1h) + { + return _impl.ElrEl1; + } + return _impl.Pc; + } + } /// public long TpidrEl0 @@ -48,6 +59,9 @@ namespace Ryujinx.Cpu.AppleHv set => _impl.Fpsr = value; } + /// + public ulong ThreadUid { get; set; } + /// public bool IsAarch32 { @@ -67,6 +81,7 @@ namespace Ryujinx.Cpu.AppleHv private readonly ICounter _counter; private readonly IHvExecutionContext _shadowContext; private IHvExecutionContext _impl; + private int _shouldStep; private readonly ExceptionCallbacks _exceptionCallbacks; @@ -103,6 +118,11 @@ namespace Ryujinx.Cpu.AppleHv _exceptionCallbacks.BreakCallback?.Invoke(this, address, imm); } + private void StepHandler() + { + _exceptionCallbacks.StepCallback?.Invoke(this); + } + private void SupervisorCallHandler(ulong address, int imm) { _exceptionCallbacks.SupervisorCallback?.Invoke(this, address, imm); @@ -127,6 +147,30 @@ namespace Ryujinx.Cpu.AppleHv return Interlocked.Exchange(ref _interruptRequested, 0) != 0; } + /// + public void RequestDebugStep() + { + Interlocked.Exchange(ref _shouldStep, 1); + } + + /// + public ulong DebugPc + { + get => Pc; + set + { + uint currentEl = Pstate & ~((uint)ExceptionLevel.PstateMask); + if (currentEl == (uint)ExceptionLevel.EL1h) + { + _impl.ElrEl1 = value; + } + else + { + _impl.Pc = value; + } + } + } + /// public void StopRunning() { @@ -142,6 +186,22 @@ namespace Ryujinx.Cpu.AppleHv while (Running) { + if (Interlocked.CompareExchange(ref _shouldStep, 0, 1) == 1) + { + uint currentEl = Pstate & ~((uint)ExceptionLevel.PstateMask); + if (currentEl == (uint)ExceptionLevel.EL1h) + { + HvApi.hv_vcpu_get_sys_reg(vcpu.Handle, HvSysReg.SPSR_EL1, out ulong spsr).ThrowOnError(); + spsr |= (1 << 21); + HvApi.hv_vcpu_set_sys_reg(vcpu.Handle, HvSysReg.SPSR_EL1, spsr); + } + else + { + Pstate |= (1 << 21); + } + HvApi.hv_vcpu_set_sys_reg(vcpu.Handle, HvSysReg.MDSCR_EL1, 1); + } + HvApi.hv_vcpu_run(vcpu.Handle).ThrowOnError(); HvExitReason reason = vcpu.ExitInfo->Reason; @@ -209,6 +269,20 @@ namespace Ryujinx.Cpu.AppleHv SupervisorCallHandler(elr - 4UL, id); vcpu = RentFromPool(memoryManager.AddressSpace, vcpu); break; + case ExceptionClass.SoftwareStepLowerEl: + HvApi.hv_vcpu_get_sys_reg(vcpuHandle, HvSysReg.SPSR_EL1, out ulong spsr).ThrowOnError(); + spsr &= ~((ulong)(1 << 21)); + HvApi.hv_vcpu_set_sys_reg(vcpuHandle, HvSysReg.SPSR_EL1, spsr).ThrowOnError(); + HvApi.hv_vcpu_set_sys_reg(vcpuHandle, HvSysReg.MDSCR_EL1, 0); + ReturnToPool(vcpu); + StepHandler(); + vcpu = RentFromPool(memoryManager.AddressSpace, vcpu); + break; + case ExceptionClass.BrkAarch64: + ReturnToPool(vcpu); + BreakHandler(elr, (ushort)esr); + vcpu = RentFromPool(memoryManager.AddressSpace, vcpu); + break; default: throw new Exception($"Unhandled guest exception {ec}."); } @@ -219,10 +293,7 @@ namespace Ryujinx.Cpu.AppleHv // TODO: Invalidate only the range that was modified? return HvAddressSpace.KernelRegionTlbiEretAddress; } - else - { - return HvAddressSpace.KernelRegionEretAddress; - } + return HvAddressSpace.KernelRegionEretAddress; } private static void DataAbort(MemoryTracking tracking, ulong vcpu, uint esr) diff --git a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs index 6ce8e1800..4ea5f276d 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextShadow.cs @@ -18,6 +18,8 @@ namespace Ryujinx.Cpu.AppleHv public bool IsAarch32 { get; set; } + public ulong ThreadUid { get; set; } + private readonly ulong[] _x; private readonly V128[] _v; @@ -46,5 +48,14 @@ namespace Ryujinx.Cpu.AppleHv { _v[index] = value; } + + public void RequestInterrupt() + { + } + + public bool GetAndClearInterruptRequested() + { + return false; + } } } diff --git a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs index 1949cabdf..9ef03e61e 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvExecutionContextVcpu.cs @@ -2,6 +2,7 @@ using ARMeilleure.State; using Ryujinx.Memory; using System.Runtime.InteropServices; using System.Runtime.Versioning; +using System.Threading; namespace Ryujinx.Cpu.AppleHv { @@ -13,6 +14,8 @@ namespace Ryujinx.Cpu.AppleHv private static readonly SetSimdFpReg _setSimdFpReg; private static readonly nint _setSimdFpRegNativePtr; + public ulong ThreadUid { get; set; } + static HvExecutionContextVcpu() { // .NET does not support passing vectors by value, so we need to pass a pointer and use a native @@ -135,6 +138,7 @@ namespace Ryujinx.Cpu.AppleHv } private readonly ulong _vcpu; + private int _interruptRequested; public HvExecutionContextVcpu(ulong vcpu) { @@ -180,8 +184,16 @@ namespace Ryujinx.Cpu.AppleHv public void RequestInterrupt() { - ulong vcpu = _vcpu; - HvApi.hv_vcpus_exit(ref vcpu, 1); + if (Interlocked.Exchange(ref _interruptRequested, 1) == 0) + { + ulong vcpu = _vcpu; + HvApi.hv_vcpus_exit(ref vcpu, 1); + } + } + + public bool GetAndClearInterruptRequested() + { + return Interlocked.Exchange(ref _interruptRequested, 0) != 0; } } } diff --git a/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs b/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs index 54b73acc6..134405b5c 100644 --- a/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs +++ b/src/Ryujinx.Cpu/AppleHv/IHvExecutionContext.cs @@ -15,6 +15,7 @@ namespace Ryujinx.Cpu.AppleHv uint Fpcr { get; set; } uint Fpsr { get; set; } + ulong ThreadUid { get; set; } ulong GetX(int index); void SetX(int index, ulong value); @@ -39,5 +40,8 @@ namespace Ryujinx.Cpu.AppleHv SetV(i, context.GetV(i)); } } + + void RequestInterrupt(); + bool GetAndClearInterruptRequested(); } } diff --git a/src/Ryujinx.Cpu/ExceptionCallbacks.cs b/src/Ryujinx.Cpu/ExceptionCallbacks.cs index d9293302b..6e50b4d70 100644 --- a/src/Ryujinx.Cpu/ExceptionCallbacks.cs +++ b/src/Ryujinx.Cpu/ExceptionCallbacks.cs @@ -29,6 +29,11 @@ namespace Ryujinx.Cpu /// public readonly ExceptionCallback BreakCallback; + /// + /// Handler for CPU software interrupts caused by single-stepping. + /// + public readonly ExceptionCallbackNoArgs StepCallback; + /// /// Handler for CPU software interrupts caused by the Arm SVC instruction. /// @@ -47,16 +52,19 @@ namespace Ryujinx.Cpu /// /// Handler for CPU interrupts triggered using /// Handler for CPU software interrupts caused by the Arm BRK instruction + /// Handler for CPU software interrupts caused by single-stepping /// Handler for CPU software interrupts caused by the Arm SVC instruction /// Handler for CPU software interrupts caused by any undefined Arm instruction public ExceptionCallbacks( ExceptionCallbackNoArgs interruptCallback = null, ExceptionCallback breakCallback = null, + ExceptionCallbackNoArgs stepCallback = null, ExceptionCallback supervisorCallback = null, ExceptionCallback undefinedCallback = null) { InterruptCallback = interruptCallback; BreakCallback = breakCallback; + StepCallback = stepCallback; SupervisorCallback = supervisorCallback; UndefinedCallback = undefinedCallback; } diff --git a/src/Ryujinx.Cpu/IExecutionContext.cs b/src/Ryujinx.Cpu/IExecutionContext.cs index c38210800..df0c94278 100644 --- a/src/Ryujinx.Cpu/IExecutionContext.cs +++ b/src/Ryujinx.Cpu/IExecutionContext.cs @@ -1,5 +1,6 @@ using ARMeilleure.State; using System; +using System.Threading; namespace Ryujinx.Cpu { @@ -46,6 +47,11 @@ namespace Ryujinx.Cpu /// bool IsAarch32 { get; set; } + /// + /// Thread UID. + /// + public ulong ThreadUid { get; set; } + /// /// Indicates whenever the CPU is still running code. /// @@ -108,5 +114,23 @@ namespace Ryujinx.Cpu /// If you only need to pause the thread temporarily, use instead. /// void StopRunning(); + + /// + /// Requests the thread to stop running temporarily and call . + /// + /// + /// The thread might not pause immediately. + /// One must not assume that guest code is no longer being executed by the thread after calling this function. + /// After single stepping, the thread should call call . + /// + void RequestDebugStep(); + + /// + /// Current Program Counter (for debugging). + /// + /// + /// PC register for the debugger. Must not be accessed while the thread isn't stopped for debugging. + /// + ulong DebugPc { get; set; } } } diff --git a/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs b/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs index f15486e68..f00acc1d7 100644 --- a/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs +++ b/src/Ryujinx.Cpu/Jit/JitExecutionContext.cs @@ -1,5 +1,6 @@ using ARMeilleure.Memory; using ARMeilleure.State; +using ExecutionContext = ARMeilleure.State.ExecutionContext; namespace Ryujinx.Cpu.Jit { @@ -53,6 +54,13 @@ namespace Ryujinx.Cpu.Jit set => _impl.IsAarch32 = value; } + /// + public ulong ThreadUid + { + get => _impl.ThreadUid; + set => _impl.ThreadUid = value; + } + /// public bool Running => _impl.Running; @@ -65,6 +73,7 @@ namespace Ryujinx.Cpu.Jit counter, InterruptHandler, BreakHandler, + StepHandler, SupervisorCallHandler, UndefinedHandler); @@ -93,6 +102,11 @@ namespace Ryujinx.Cpu.Jit _exceptionCallbacks.BreakCallback?.Invoke(this, address, imm); } + private void StepHandler(ExecutionContext context) + { + _exceptionCallbacks.StepCallback?.Invoke(this); + } + private void SupervisorCallHandler(ExecutionContext context, ulong address, int imm) { _exceptionCallbacks.SupervisorCallback?.Invoke(this, address, imm); @@ -109,6 +123,16 @@ namespace Ryujinx.Cpu.Jit _impl.RequestInterrupt(); } + /// + public void RequestDebugStep() => _impl.RequestDebugStep(); + + /// + public ulong DebugPc + { + get => _impl.DebugPc; + set => _impl.DebugPc = value; + } + /// public void StopRunning() { diff --git a/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs b/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs index a366dcca6..a1ba0002e 100644 --- a/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs +++ b/src/Ryujinx.Cpu/LightningJit/State/ExecutionContext.cs @@ -1,6 +1,8 @@ +using ARMeilleure; using ARMeilleure.Memory; using ARMeilleure.State; using System; +using System.Threading; namespace Ryujinx.Cpu.LightningJit.State { @@ -51,6 +53,8 @@ namespace Ryujinx.Cpu.LightningJit.State } public bool IsAarch32 { get; set; } + + public ulong ThreadUid { get; set; } internal ExecutionMode ExecutionMode { @@ -77,15 +81,20 @@ namespace Ryujinx.Cpu.LightningJit.State private readonly ExceptionCallbackNoArgs _interruptCallback; private readonly ExceptionCallback _breakCallback; + private readonly ExceptionCallbackNoArgs _stepCallback; private readonly ExceptionCallback _supervisorCallback; private readonly ExceptionCallback _undefinedCallback; + internal int ShouldStep; + public ulong DebugPc { get; set; } + public ExecutionContext(IJitMemoryAllocator allocator, ICounter counter, ExceptionCallbacks exceptionCallbacks) { _nativeContext = new NativeContext(allocator); _counter = counter; _interruptCallback = exceptionCallbacks.InterruptCallback; _breakCallback = exceptionCallbacks.BreakCallback; + _stepCallback = exceptionCallbacks.StepCallback; _supervisorCallback = exceptionCallbacks.SupervisorCallback; _undefinedCallback = exceptionCallbacks.UndefinedCallback; @@ -117,6 +126,17 @@ namespace Ryujinx.Cpu.LightningJit.State _interrupted = true; } + public void StepHandler() + { + _stepCallback?.Invoke(this); + } + + public void RequestDebugStep() + { + Interlocked.Exchange(ref ShouldStep, 1); + RequestInterrupt(); + } + internal void OnBreak(ulong address, int imm) { _breakCallback?.Invoke(this, address, imm); diff --git a/src/Ryujinx.HLE/Debugger/BreakpointManager.cs b/src/Ryujinx.HLE/Debugger/BreakpointManager.cs new file mode 100644 index 000000000..bf462a781 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/BreakpointManager.cs @@ -0,0 +1,203 @@ +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.Memory; +using System.Collections.Concurrent; +using System.Linq; + +namespace Ryujinx.HLE.Debugger +{ + internal class Breakpoint + { + public byte[] OriginalData { get; } + + public bool IsStep { get; } + + public Breakpoint(byte[] originalData, bool isStep) + { + OriginalData = originalData; + IsStep = isStep; + } + } + + /// + /// Manages software breakpoints for the debugger. + /// + public class BreakpointManager + { + private readonly Debugger _debugger; + private readonly ConcurrentDictionary _breakpoints = new(); + + private static readonly byte[] _aarch64BreakInstruction = { 0x00, 0x00, 0x20, 0xD4 }; // BRK #0 + private static readonly byte[] _aarch32BreakInstruction = { 0xFE, 0xDE, 0xFF, 0xE7 }; // TRAP + private static readonly byte[] _aarch32ThumbBreakInstruction = { 0x80, 0xB6 }; + + public BreakpointManager(Debugger debugger) + { + _debugger = debugger; + } + + /// + /// Sets a software breakpoint at a specified address. + /// + /// The memory address to set the breakpoint at. + /// The length of the instruction to replace. + /// Indicates if this is a single-step breakpoint. + /// True if the breakpoint was set successfully; otherwise, false. + public bool SetBreakPoint(ulong address, ulong length, bool isStep = false) + { + if (_breakpoints.ContainsKey(address)) + { + return false; + } + + byte[] breakInstruction = GetBreakInstruction(length); + if (breakInstruction == null) + { + Logger.Error?.Print(LogClass.GdbStub, $"Unsupported instruction length for breakpoint: {length}"); + return false; + } + + var originalInstruction = new byte[length]; + if (!ReadMemory(address, originalInstruction)) + { + Logger.Error?.Print(LogClass.GdbStub, $"Failed to read memory at 0x{address:X16} to set breakpoint."); + return false; + } + + if (!WriteMemory(address, breakInstruction)) + { + Logger.Error?.Print(LogClass.GdbStub, $"Failed to write breakpoint at 0x{address:X16}."); + return false; + } + + var breakpoint = new Breakpoint(originalInstruction, isStep); + if (_breakpoints.TryAdd(address, breakpoint)) + { + Logger.Debug?.Print(LogClass.GdbStub, $"Breakpoint set at 0x{address:X16}"); + return true; + } + + Logger.Error?.Print(LogClass.GdbStub, $"Failed to add breakpoint at 0x{address:X16}."); + return false; + } + + /// + /// Clears a software breakpoint at a specified address. + /// + /// The memory address of the breakpoint to clear. + /// The length of the instruction (unused). + /// True if the breakpoint was cleared successfully; otherwise, false. + public bool ClearBreakPoint(ulong address, ulong length) + { + if (_breakpoints.TryGetValue(address, out Breakpoint breakpoint)) + { + if (!WriteMemory(address, breakpoint.OriginalData)) + { + Logger.Error?.Print(LogClass.GdbStub, $"Failed to restore original instruction at 0x{address:X16} to clear breakpoint."); + return false; + } + + _breakpoints.TryRemove(address, out _); + Logger.Debug?.Print(LogClass.GdbStub, $"Breakpoint cleared at 0x{address:X16}"); + return true; + } + + Logger.Warning?.Print(LogClass.GdbStub, $"No breakpoint found at address 0x{address:X16}"); + return false; + } + + /// + /// Clears all currently set software breakpoints. + /// + public void ClearAll() + { + foreach (var bp in _breakpoints) + { + if (!WriteMemory(bp.Key, bp.Value.OriginalData)) + { + Logger.Error?.Print(LogClass.GdbStub, $"Failed to restore original instruction at 0x{bp.Key:X16} while clearing all breakpoints."); + } + + } + _breakpoints.Clear(); + Logger.Debug?.Print(LogClass.GdbStub, "All breakpoints cleared."); + } + + /// + /// Clears all currently set single-step software breakpoints. + /// + public void ClearAllStepBreakpoints() + { + var stepBreakpoints = _breakpoints.Where(p => p.Value.IsStep).ToList(); + + if (stepBreakpoints.Count == 0) + { + return; + } + + foreach (var bp in stepBreakpoints) + { + if (_breakpoints.TryRemove(bp.Key, out Breakpoint removedBreakpoint)) + { + WriteMemory(bp.Key, removedBreakpoint.OriginalData); + } + } + + Logger.Debug?.Print(LogClass.GdbStub, "All step breakpoints cleared."); + } + + + private byte[] GetBreakInstruction(ulong length) + { + if (_debugger.IsProcessAarch32) + { + if (length == 2) + { + return _aarch32ThumbBreakInstruction; + } + + if (length == 4) + { + return _aarch32BreakInstruction; + } + } + else + { + if (length == 4) + { + return _aarch64BreakInstruction; + } + } + + return null; + } + + private bool ReadMemory(ulong address, byte[] data) + { + try + { + _debugger.DebugProcess.CpuMemory.Read(address, data); + return true; + } + catch (InvalidMemoryRegionException) + { + return false; + } + } + + private bool WriteMemory(ulong address, byte[] data) + { + try + { + _debugger.DebugProcess.CpuMemory.Write(address, data); + _debugger.DebugProcess.InvalidateCacheRegion(address, (ulong)data.Length); + return true; + } + catch (InvalidMemoryRegionException) + { + return false; + } + } + } +} diff --git a/src/Ryujinx.HLE/Debugger/DebugState.cs b/src/Ryujinx.HLE/Debugger/DebugState.cs new file mode 100644 index 000000000..d2efa2bff --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/DebugState.cs @@ -0,0 +1,9 @@ +namespace Ryujinx.HLE.Debugger +{ + public enum DebugState + { + Running, + Stopping, + Stopped, + } +} diff --git a/src/Ryujinx.HLE/Debugger/Debugger.cs b/src/Ryujinx.HLE/Debugger/Debugger.cs new file mode 100644 index 000000000..7a626b840 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Debugger.cs @@ -0,0 +1,1327 @@ +using ARMeilleure.State; +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.HLE.HOS.Kernel; +using Ryujinx.HLE.HOS.Kernel.Process; +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.Memory; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using IExecutionContext = Ryujinx.Cpu.IExecutionContext; + +namespace Ryujinx.HLE.Debugger +{ + public class Debugger : IDisposable + { + internal Switch Device { get; private set; } + + public ushort GdbStubPort { get; private set; } + + private TcpListener ListenerSocket; + private Socket ClientSocket = null; + private NetworkStream ReadStream = null; + private NetworkStream WriteStream = null; + private BlockingCollection Messages = new BlockingCollection(1); + private Thread DebuggerThread; + private Thread MessageHandlerThread; + private bool _shuttingDown = false; + private ManualResetEventSlim _breakHandlerEvent = new ManualResetEventSlim(false); + + private ulong? cThread; + private ulong? gThread; + + private BreakpointManager BreakpointManager; + + private string previousThreadListXml = ""; + + public Debugger(Switch device, ushort port) + { + Device = device; + GdbStubPort = port; + + ARMeilleure.Optimizations.EnableDebugging = true; + + DebuggerThread = new Thread(DebuggerThreadMain); + DebuggerThread.Start(); + MessageHandlerThread = new Thread(MessageHandlerMain); + MessageHandlerThread.Start(); + BreakpointManager = new BreakpointManager(this); + } + + internal KProcess Process => Device.System?.DebugGetApplicationProcess(); + internal IDebuggableProcess DebugProcess => Device.System?.DebugGetApplicationProcessDebugInterface(); + private KThread[] GetThreads() => DebugProcess.GetThreadUids().Select(x => DebugProcess.GetThread(x)).ToArray(); + internal bool IsProcessAarch32 => DebugProcess.GetThread(gThread.Value).Context.IsAarch32; + private KernelContext KernelContext => Device.System.KernelContext; + + const int GdbRegisterCount64 = 68; + const int GdbRegisterCount32 = 66; + /* FPCR = FPSR & ~FpcrMask + All of FPCR's bits are reserved in FPCR and vice versa, + see ARM's documentation. */ + private const uint FpcrMask = 0xfc1fffff; + + private string GdbReadRegister64(IExecutionContext state, int gdbRegId) + { + switch (gdbRegId) + { + case >= 0 and <= 31: + return ToHex(BitConverter.GetBytes(state.GetX(gdbRegId))); + case 32: + return ToHex(BitConverter.GetBytes(state.DebugPc)); + case 33: + return ToHex(BitConverter.GetBytes(state.Pstate)); + case >= 34 and <= 65: + return ToHex(state.GetV(gdbRegId - 34).ToArray()); + case 66: + return ToHex(BitConverter.GetBytes((uint)state.Fpsr)); + case 67: + return ToHex(BitConverter.GetBytes((uint)state.Fpcr)); + default: + return null; + } + } + + private bool GdbWriteRegister64(IExecutionContext state, int gdbRegId, StringStream ss) + { + switch (gdbRegId) + { + case >= 0 and <= 31: + { + ulong value = ss.ReadLengthAsLEHex(16); + state.SetX(gdbRegId, value); + return true; + } + case 32: + { + ulong value = ss.ReadLengthAsLEHex(16); + state.DebugPc = value; + return true; + } + case 33: + { + ulong value = ss.ReadLengthAsLEHex(8); + state.Pstate = (uint)value; + return true; + } + case >= 34 and <= 65: + { + ulong value0 = ss.ReadLengthAsLEHex(16); + ulong value1 = ss.ReadLengthAsLEHex(16); + state.SetV(gdbRegId - 34, new V128(value0, value1)); + return true; + } + case 66: + { + ulong value = ss.ReadLengthAsLEHex(8); + state.Fpsr = (uint)value; + return true; + } + case 67: + { + ulong value = ss.ReadLengthAsLEHex(8); + state.Fpcr = (uint)value; + return true; + } + default: + return false; + } + } + + private string GdbReadRegister32(IExecutionContext state, int gdbRegId) + { + switch (gdbRegId) + { + case >= 0 and <= 14: + return ToHex(BitConverter.GetBytes((uint)state.GetX(gdbRegId))); + case 15: + return ToHex(BitConverter.GetBytes((uint)state.DebugPc)); + case 16: + return ToHex(BitConverter.GetBytes((uint)state.Pstate)); + case >= 17 and <= 32: + return ToHex(state.GetV(gdbRegId - 17).ToArray()); + case >= 33 and <= 64: + int reg = (gdbRegId - 33); + int n = reg / 2; + int shift = reg % 2; + ulong value = state.GetV(n).Extract(shift); + return ToHex(BitConverter.GetBytes(value)); + case 65: + uint fpscr = (uint)state.Fpsr | (uint)state.Fpcr; + return ToHex(BitConverter.GetBytes(fpscr)); + default: + return null; + } + } + + private bool GdbWriteRegister32(IExecutionContext state, int gdbRegId, StringStream ss) + { + switch (gdbRegId) + { + case >= 0 and <= 14: + { + ulong value = ss.ReadLengthAsLEHex(8); + state.SetX(gdbRegId, value); + return true; + } + case 15: + { + ulong value = ss.ReadLengthAsLEHex(8); + state.DebugPc = value; + return true; + } + case 16: + { + ulong value = ss.ReadLengthAsLEHex(8); + state.Pstate = (uint)value; + return true; + } + case >= 17 and <= 32: + { + ulong value0 = ss.ReadLengthAsLEHex(16); + ulong value1 = ss.ReadLengthAsLEHex(16); + state.SetV(gdbRegId - 17, new V128(value0, value1)); + return true; + } + case >= 33 and <= 64: + { + ulong value = ss.ReadLengthAsLEHex(16); + int regId = (gdbRegId - 33); + int regNum = regId / 2; + int shift = regId % 2; + V128 reg = state.GetV(regNum); + reg.Insert(shift, value); + return true; + } + case 65: + { + ulong value = ss.ReadLengthAsLEHex(8); + state.Fpsr = (uint)value & FpcrMask; + state.Fpcr = (uint)value & ~FpcrMask; + return true; + } + default: + return false; + } + } + + private void MessageHandlerMain() + { + while (!_shuttingDown) + { + IMessage msg = Messages.Take(); + try { + switch (msg) + { + case BreakInMessage: + Logger.Notice.Print(LogClass.GdbStub, "Break-in requested"); + CommandInterrupt(); + break; + + case SendNackMessage: + WriteStream.WriteByte((byte)'-'); + break; + + case CommandMessage { Command: var cmd }: + Logger.Debug?.Print(LogClass.GdbStub, $"Received Command: {cmd}"); + WriteStream.WriteByte((byte)'+'); + ProcessCommand(cmd); + break; + + case ThreadBreakMessage { Context: var ctx }: + DebugProcess.DebugStop(); + gThread = cThread = ctx.ThreadUid; + _breakHandlerEvent.Set(); + Reply($"T05thread:{ctx.ThreadUid:x};"); + break; + + case KillMessage: + return; + } + } + catch (IOException e) + { + Logger.Error?.Print(LogClass.GdbStub, "Error while processing GDB messages", e); + } + catch (NullReferenceException e) + { + Logger.Error?.Print(LogClass.GdbStub, "Error while processing GDB messages", e); + } + } + } + + private void ProcessCommand(string cmd) + { + StringStream ss = new StringStream(cmd); + + switch (ss.ReadChar()) + { + case '!': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + // Enable extended mode + ReplyOK(); + break; + case '?': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + CommandQuery(); + break; + case 'c': + CommandContinue(ss.IsEmpty() ? null : ss.ReadRemainingAsHex()); + break; + case 'D': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + CommandDetach(); + break; + case 'g': + if (!ss.IsEmpty()) + { + goto unknownCommand; + } + + CommandReadRegisters(); + break; + case 'G': + CommandWriteRegisters(ss); + break; + case 'H': + { + char op = ss.ReadChar(); + ulong? threadId = ss.ReadRemainingAsThreadUid(); + CommandSetThread(op, threadId); + break; + } + case 'k': + Logger.Notice.Print(LogClass.GdbStub, "Kill request received, detach instead"); + Reply(""); + CommandDetach(); + break; + case 'm': + { + ulong addr = ss.ReadUntilAsHex(','); + ulong len = ss.ReadRemainingAsHex(); + CommandReadMemory(addr, len); + break; + } + case 'M': + { + ulong addr = ss.ReadUntilAsHex(','); + ulong len = ss.ReadUntilAsHex(':'); + CommandWriteMemory(addr, len, ss); + break; + } + case 'p': + { + ulong gdbRegId = ss.ReadRemainingAsHex(); + CommandReadRegister((int)gdbRegId); + break; + } + case 'P': + { + ulong gdbRegId = ss.ReadUntilAsHex('='); + CommandWriteRegister((int)gdbRegId, ss); + break; + } + case 'q': + if (ss.ConsumeRemaining("GDBServerVersion")) + { + Reply($"name:Ryujinx;version:{ReleaseInformation.Version};"); + break; + } + + if (ss.ConsumeRemaining("HostInfo")) + { + if (IsProcessAarch32) + { + Reply( + $"triple:{ToHex("arm-unknown-linux-android")};endian:little;ptrsize:4;hostname:{ToHex("Ryujinx")};"); + } + else + { + Reply( + $"triple:{ToHex("aarch64-unknown-linux-android")};endian:little;ptrsize:8;hostname:{ToHex("Ryujinx")};"); + } + break; + } + + if (ss.ConsumeRemaining("ProcessInfo")) + { + if (IsProcessAarch32) + { + Reply( + $"pid:1;cputype:12;cpusubtype:0;triple:{ToHex("arm-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:4;"); + } + else + { + Reply( + $"pid:1;cputype:100000c;cpusubtype:0;triple:{ToHex("aarch64-unknown-linux-android")};ostype:unknown;vendor:none;endian:little;ptrsize:8;"); + } + break; + } + + if (ss.ConsumePrefix("Supported:") || ss.ConsumeRemaining("Supported")) + { + Reply("PacketSize=10000;qXfer:features:read+;qXfer:threads:read+;vContSupported+"); + break; + } + + if (ss.ConsumePrefix("Rcmd,")) + { + string hexCommand = ss.ReadRemaining(); + HandleQRcmdCommand(hexCommand); + break; + } + + if (ss.ConsumeRemaining("fThreadInfo")) + { + Reply($"m{string.Join(",", DebugProcess.GetThreadUids().Select(x => $"{x:x}"))}"); + break; + } + + if (ss.ConsumeRemaining("sThreadInfo")) + { + Reply("l"); + break; + } + + if (ss.ConsumePrefix("ThreadExtraInfo,")) + { + ulong? threadId = ss.ReadRemainingAsThreadUid(); + if (threadId == null) + { + ReplyError(); + break; + } + + if (DebugProcess.IsThreadPaused(DebugProcess.GetThread(threadId.Value))) + { + Reply(ToHex("Paused")); + } + else + { + Reply(ToHex("Running")); + } + break; + } + + if (ss.ConsumePrefix("Xfer:threads:read:")) + { + ss.ReadUntil(':'); + ulong offset = ss.ReadUntilAsHex(','); + ulong len = ss.ReadRemainingAsHex(); + + var data = ""; + if (offset > 0) + { + data = previousThreadListXml; + } else + { + previousThreadListXml = data = GetThreadListXml(); + } + + if (offset >= (ulong)data.Length) + { + Reply("l"); + break; + } + + if (len >= (ulong)data.Length - offset) + { + Reply("l" + ToBinaryFormat(data.Substring((int)offset))); + break; + } + else + { + Reply("m" + ToBinaryFormat(data.Substring((int)offset, (int)len))); + break; + } + } + + if (ss.ConsumePrefix("Xfer:features:read:")) + { + string feature = ss.ReadUntil(':'); + ulong offset = ss.ReadUntilAsHex(','); + ulong len = ss.ReadRemainingAsHex(); + + if (feature == "target.xml") + { + feature = IsProcessAarch32 ? "target32.xml" : "target64.xml"; + } + + string data; + if (RegisterInformation.Features.TryGetValue(feature, out data)) + { + if (offset >= (ulong)data.Length) + { + Reply("l"); + break; + } + + if (len >= (ulong)data.Length - offset) + { + Reply("l" + ToBinaryFormat(data.Substring((int)offset))); + break; + } + else + { + Reply("m" + ToBinaryFormat(data.Substring((int)offset, (int)len))); + break; + } + } + else + { + Reply("E00"); // Invalid annex + break; + } + } + + goto unknownCommand; + case 'Q': + goto unknownCommand; + case 's': + CommandStep(ss.IsEmpty() ? null : ss.ReadRemainingAsHex()); + break; + case 'T': + { + ulong? threadId = ss.ReadRemainingAsThreadUid(); + CommandIsAlive(threadId); + break; + } + case 'v': + if (ss.ConsumePrefix("Cont")) + { + if (ss.ConsumeRemaining("?")) + { + Reply("vCont;c;C;s;S"); + break; + } + + if (ss.ConsumePrefix(";")) + { + HandleVContCommand(ss); + break; + } + + goto unknownCommand; + } + if (ss.ConsumeRemaining("MustReplyEmpty")) + { + Reply(""); + break; + } + goto unknownCommand; + case 'Z': + { + string type = ss.ReadUntil(','); + ulong addr = ss.ReadUntilAsHex(','); + ulong len = ss.ReadLengthAsHex(1); + string extra = ss.ReadRemaining(); + + if (extra.Length > 0) + { + Logger.Notice.Print(LogClass.GdbStub, $"Unsupported Z command extra data: {extra}"); + ReplyError(); + return; + } + + switch (type) + { + case "0": // Software breakpoint + if (!BreakpointManager.SetBreakPoint(addr, len, false)) + { + ReplyError(); + return; + } + ReplyOK(); + return; + case "1": // Hardware breakpoint + case "2": // Write watchpoint + case "3": // Read watchpoint + case "4": // Access watchpoint + ReplyError(); + return; + default: + ReplyError(); + return; + } + } + case 'z': + { + string type = ss.ReadUntil(','); + ss.ConsumePrefix(","); + ulong addr = ss.ReadUntilAsHex(','); + ulong len = ss.ReadLengthAsHex(1); + string extra = ss.ReadRemaining(); + + if (extra.Length > 0) + { + Logger.Notice.Print(LogClass.GdbStub, $"Unsupported z command extra data: {extra}"); + ReplyError(); + return; + } + + switch (type) + { + case "0": // Software breakpoint + if (!BreakpointManager.ClearBreakPoint(addr, len)) + { + ReplyError(); + return; + } + ReplyOK(); + return; + case "1": // Hardware breakpoint + case "2": // Write watchpoint + case "3": // Read watchpoint + case "4": // Access watchpoint + ReplyError(); + return; + default: + ReplyError(); + return; + } + } + default: + unknownCommand: + Logger.Notice.Print(LogClass.GdbStub, $"Unknown command: {cmd}"); + Reply(""); + break; + } + } + + enum VContAction + { + None, + Continue, + Stop, + Step + } + + record VContPendingAction(VContAction Action, ushort? Signal = null); + + private void HandleVContCommand(StringStream ss) + { + string[] rawActions = ss.ReadRemaining().Split(';', StringSplitOptions.RemoveEmptyEntries); + + var threadActionMap = new Dictionary(); + foreach (var thread in GetThreads()) + { + threadActionMap[thread.ThreadUid] = new VContPendingAction(VContAction.None); + } + + VContAction defaultAction = VContAction.None; + + // For each inferior thread, the *leftmost* action with a matching thread-id is applied. + for (int i = rawActions.Length - 1; i >= 0; i--) + { + var rawAction = rawActions[i]; + var stream = new StringStream(rawAction); + + char cmd = stream.ReadChar(); + VContAction action = cmd switch + { + 'c' => VContAction.Continue, + 'C' => VContAction.Continue, + 's' => VContAction.Step, + 'S' => VContAction.Step, + 't' => VContAction.Stop, + _ => VContAction.None + }; + + // Note: We don't support signals yet. + ushort? signal = null; + if (cmd == 'C' || cmd == 'S') + { + signal = (ushort)stream.ReadLengthAsHex(2); + } + + ulong? threadId = null; + if (stream.ConsumePrefix(":")) + { + threadId = stream.ReadRemainingAsThreadUid(); + } + + if (threadId.HasValue) + { + if (threadActionMap.ContainsKey(threadId.Value)) { + threadActionMap[threadId.Value] = new VContPendingAction(action, signal); + } + } + else + { + foreach (var row in threadActionMap.ToList()) + { + threadActionMap[row.Key] = new VContPendingAction(action, signal); + } + + if (action == VContAction.Continue) { + defaultAction = action; + } else { + Logger.Warning?.Print(LogClass.GdbStub, $"Received vCont command with unsupported default action: {rawAction}"); + } + } + } + + bool hasError = false; + + foreach (var (threadUid, action) in threadActionMap) + { + if (action.Action == VContAction.Step) + { + var thread = DebugProcess.GetThread(threadUid); + if (!DebugProcess.DebugStep(thread)) { + hasError = true; + } + } + } + + // If we receive "vCont;c", just continue the process. + // If we receive something like "vCont;c:2e;c:2f" (IDA Pro will send commands like this), continue these threads. + // For "vCont;s:2f;c", `DebugProcess.DebugStep()` will continue and suspend other threads if needed, so we don't do anything here. + if (threadActionMap.Values.All(a => a.Action == VContAction.Continue)) + { + DebugProcess.DebugContinue(); + } else if (defaultAction == VContAction.None) { + foreach (var (threadUid, action) in threadActionMap) + { + if (action.Action == VContAction.Continue) + { + DebugProcess.DebugContinue(DebugProcess.GetThread(threadUid)); + } + } + } + + if (hasError) + { + ReplyError(); + } + else + { + ReplyOK(); + } + + foreach (var (threadUid, action) in threadActionMap) + { + if (action.Action == VContAction.Step) + { + gThread = cThread = threadUid; + Reply($"T05thread:{threadUid:x};"); + } + } + } + + private string GetThreadListXml() + { + var sb = new StringBuilder(); + sb.Append("\n"); + + foreach (var thread in GetThreads()) + { + string threadName = System.Security.SecurityElement.Escape(thread.GetThreadName()); + sb.Append($"{(DebugProcess.IsThreadPaused(thread) ? "Paused" : "Running")}\n"); + } + + sb.Append(""); + return sb.ToString(); + } + + void CommandQuery() + { + // GDB is performing initial contact. Stop everything. + DebugProcess.DebugStop(); + gThread = cThread = DebugProcess.GetThreadUids().First(); + Reply($"T05thread:{cThread:x};"); + } + + void CommandInterrupt() + { + // GDB is requesting an interrupt. Stop everything. + DebugProcess.DebugStop(); + if (gThread == null || !GetThreads().Any(x => x.ThreadUid == gThread.Value)) + { + gThread = cThread = DebugProcess.GetThreadUids().First(); + } + + Reply($"T02thread:{gThread:x};"); + } + + void CommandContinue(ulong? newPc) + { + if (newPc.HasValue) + { + if (cThread == null) + { + ReplyError(); + return; + } + + DebugProcess.GetThread(cThread.Value).Context.DebugPc = newPc.Value; + } + + DebugProcess.DebugContinue(); + } + + void CommandDetach() + { + BreakpointManager.ClearAll(); + CommandContinue(null); + } + + void CommandReadRegisters() + { + if (gThread == null) + { + ReplyError(); + return; + } + + var ctx = DebugProcess.GetThread(gThread.Value).Context; + string registers = ""; + if (IsProcessAarch32) + { + for (int i = 0; i < GdbRegisterCount32; i++) + { + registers += GdbReadRegister32(ctx, i); + } + } + else + { + for (int i = 0; i < GdbRegisterCount64; i++) + { + registers += GdbReadRegister64(ctx, i); + } + } + + Reply(registers); + } + + void CommandWriteRegisters(StringStream ss) + { + if (gThread == null) + { + ReplyError(); + return; + } + + var ctx = DebugProcess.GetThread(gThread.Value).Context; + if (IsProcessAarch32) + { + for (int i = 0; i < GdbRegisterCount32; i++) + { + if (!GdbWriteRegister32(ctx, i, ss)) + { + ReplyError(); + return; + } + } + } + else + { + for (int i = 0; i < GdbRegisterCount64; i++) + { + if (!GdbWriteRegister64(ctx, i, ss)) + { + ReplyError(); + return; + } + } + } + + if (ss.IsEmpty()) + { + ReplyOK(); + } + else + { + ReplyError(); + } + } + + void CommandSetThread(char op, ulong? threadId) + { + if (threadId == 0 || threadId == null) + { + threadId = GetThreads().First().ThreadUid; + } + + if (DebugProcess.GetThread(threadId.Value) == null) + { + ReplyError(); + return; + } + + switch (op) + { + case 'c': + cThread = threadId; + ReplyOK(); + return; + case 'g': + gThread = threadId; + ReplyOK(); + return; + default: + ReplyError(); + return; + } + } + + void CommandReadMemory(ulong addr, ulong len) + { + try + { + var data = new byte[len]; + DebugProcess.CpuMemory.Read(addr, data); + Reply(ToHex(data)); + } + catch (InvalidMemoryRegionException) + { + // InvalidAccessHandler will show an error message, we log it again to tell user the error is from GDB (which can be ignored) + // TODO: Do not let InvalidAccessHandler show the error message + Logger.Notice.Print(LogClass.GdbStub, $"GDB failed to read memory at 0x{addr:X16}"); + ReplyError(); + } + } + + void CommandWriteMemory(ulong addr, ulong len, StringStream ss) + { + try + { + var data = new byte[len]; + for (ulong i = 0; i < len; i++) + { + data[i] = (byte)ss.ReadLengthAsHex(2); + } + + DebugProcess.CpuMemory.Write(addr, data); + DebugProcess.InvalidateCacheRegion(addr, len); + ReplyOK(); + } + catch (InvalidMemoryRegionException) + { + ReplyError(); + } + } + + void CommandReadRegister(int gdbRegId) + { + if (gThread == null) + { + ReplyError(); + return; + } + + var ctx = DebugProcess.GetThread(gThread.Value).Context; + string result; + if (IsProcessAarch32) + { + result = GdbReadRegister32(ctx, gdbRegId); + if (result != null) + { + Reply(result); + } + else + { + ReplyError(); + } + } + else + { + result = GdbReadRegister64(ctx, gdbRegId); + if (result != null) + { + Reply(result); + } + else + { + ReplyError(); + } + } + } + + void CommandWriteRegister(int gdbRegId, StringStream ss) + { + if (gThread == null) + { + ReplyError(); + return; + } + + var ctx = DebugProcess.GetThread(gThread.Value).Context; + if (IsProcessAarch32) + { + if (GdbWriteRegister32(ctx, gdbRegId, ss) && ss.IsEmpty()) + { + ReplyOK(); + } + else + { + ReplyError(); + } + } + else + { + if (GdbWriteRegister64(ctx, gdbRegId, ss) && ss.IsEmpty()) + { + ReplyOK(); + } + else + { + ReplyError(); + } + } + } + + private void CommandStep(ulong? newPc) + { + if (cThread == null) + { + ReplyError(); + return; + } + + var thread = DebugProcess.GetThread(cThread.Value); + + if (newPc.HasValue) + { + thread.Context.DebugPc = newPc.Value; + } + + if (!DebugProcess.DebugStep(thread)) + { + ReplyError(); + } + else + { + gThread = cThread = thread.ThreadUid; + Reply($"T05thread:{thread.ThreadUid:x};"); + } + } + + private void CommandIsAlive(ulong? threadId) + { + if (GetThreads().Any(x => x.ThreadUid == threadId)) + { + ReplyOK(); + } + else + { + Reply("E00"); + } + } + + private void HandleQRcmdCommand(string hexCommand) + { + try + { + string command = FromHex(hexCommand); + Logger.Debug?.Print(LogClass.GdbStub, $"Received Rcmd: {command}"); + + string response = command.Trim().ToLowerInvariant() switch + { + "help" => "backtrace\nbt\nregisters\nreg\nget info\n", + "get info" => GetProcessInfo(), + "backtrace" => GetStackTrace(), + "bt" => GetStackTrace(), + "registers" => GetRegisters(), + "reg" => GetRegisters(), + _ => $"Unknown command: {command}\n" + }; + + Reply(ToHex(response)); + } + catch (Exception e) + { + Logger.Error?.Print(LogClass.GdbStub, $"Error processing Rcmd: {e.Message}"); + ReplyError(); + } + } + + private string GetStackTrace() + { + if (gThread == null) + return "No thread selected\n"; + + if (Process == null) + return "No application process found\n"; + + return Process.Debugger.GetGuestStackTrace(DebugProcess.GetThread(gThread.Value)); + } + + private string GetRegisters() + { + if (gThread == null) + return "No thread selected\n"; + + if (Process == null) + return "No application process found\n"; + + return Process.Debugger.GetCpuRegisterPrintout(DebugProcess.GetThread(gThread.Value)); + } + + private string GetProcessInfo() + { + try + { + if (Process == null) + return "No application process found\n"; + + KProcess kProcess = Process; + + var sb = new StringBuilder(); + + sb.AppendLine($"Program Id: 0x{kProcess.TitleId:x16}"); + sb.AppendLine($"Application: {(kProcess.IsApplication ? 1 : 0)}"); + sb.AppendLine("Layout:"); + sb.AppendLine($" Alias: 0x{kProcess.MemoryManager.AliasRegionStart:x10} - 0x{kProcess.MemoryManager.AliasRegionEnd - 1:x10}"); + sb.AppendLine($" Heap: 0x{kProcess.MemoryManager.HeapRegionStart:x10} - 0x{kProcess.MemoryManager.HeapRegionEnd - 1:x10}"); + sb.AppendLine($" Aslr: 0x{kProcess.MemoryManager.AslrRegionStart:x10} - 0x{kProcess.MemoryManager.AslrRegionEnd - 1:x10}"); + sb.AppendLine($" Stack: 0x{kProcess.MemoryManager.StackRegionStart:x10} - 0x{kProcess.MemoryManager.StackRegionEnd - 1:x10}"); + + sb.AppendLine("Modules:"); + var debugger = kProcess.Debugger; + if (debugger != null) + { + var images = debugger.GetLoadedImages(); + for (int i = 0; i < images.Count; i++) + { + var image = images[i]; + ulong endAddress = image.BaseAddress + image.Size - 1; + string name = debugger.GetGuessedNsoNameFromIndex(i); + sb.AppendLine($" 0x{image.BaseAddress:x10} - 0x{endAddress:x10} {name}"); + } + } + + return sb.ToString(); + } + catch (Exception e) + { + Logger.Error?.Print(LogClass.GdbStub, $"Error getting process info: {e.Message}"); + return $"Error getting process info: {e.Message}\n"; + } + } + + private void Reply(string cmd) + { + Logger.Debug?.Print(LogClass.GdbStub, $"Reply: {cmd}"); + WriteStream.Write(Encoding.ASCII.GetBytes($"${cmd}#{CalculateChecksum(cmd):x2}")); + } + + private void ReplyOK() + { + Reply("OK"); + } + + private void ReplyError() + { + Reply("E01"); + } + + private void DebuggerThreadMain() + { + var endpoint = new IPEndPoint(IPAddress.Any, GdbStubPort); + ListenerSocket = new TcpListener(endpoint); + ListenerSocket.Start(); + Logger.Notice.Print(LogClass.GdbStub, $"Currently waiting on {endpoint} for GDB client"); + + while (!_shuttingDown) + { + try + { + ClientSocket = ListenerSocket.AcceptSocket(); + } + catch (SocketException) + { + return; + } + + // If the user connects before the application is running, wait for the application to start. + int retries = 10; + while (DebugProcess == null && retries-- > 0) + { + Thread.Sleep(200); + } + if (DebugProcess == null) + { + Logger.Warning?.Print(LogClass.GdbStub, "Application is not running, cannot accept GDB client connection"); + ClientSocket.Close(); + continue; + } + + ClientSocket.NoDelay = true; + ReadStream = new NetworkStream(ClientSocket, System.IO.FileAccess.Read); + WriteStream = new NetworkStream(ClientSocket, System.IO.FileAccess.Write); + Logger.Notice.Print(LogClass.GdbStub, "GDB client connected"); + + while (true) + { + try + { + switch (ReadStream.ReadByte()) + { + case -1: + goto eof; + case '+': + continue; + case '-': + Logger.Notice.Print(LogClass.GdbStub, "NACK received!"); + continue; + case '\x03': + Messages.Add(new BreakInMessage()); + break; + case '$': + string cmd = ""; + while (true) + { + int x = ReadStream.ReadByte(); + if (x == -1) + goto eof; + if (x == '#') + break; + cmd += (char)x; + } + + string checksum = $"{(char)ReadStream.ReadByte()}{(char)ReadStream.ReadByte()}"; + if (checksum == $"{CalculateChecksum(cmd):x2}") + { + Messages.Add(new CommandMessage(cmd)); + } + else + { + Messages.Add(new SendNackMessage()); + } + + break; + } + } + catch (IOException) + { + goto eof; + } + } + + eof: + Logger.Notice.Print(LogClass.GdbStub, "GDB client lost connection"); + ReadStream.Close(); + ReadStream = null; + WriteStream.Close(); + WriteStream = null; + ClientSocket.Close(); + ClientSocket = null; + + BreakpointManager.ClearAll(); + } + } + + private byte CalculateChecksum(string cmd) + { + byte checksum = 0; + foreach (char x in cmd) + { + unchecked + { + checksum += (byte)x; + } + } + + return checksum; + } + + private string FromHex(string hexString) + { + if (string.IsNullOrEmpty(hexString)) + return string.Empty; + + byte[] bytes = Convert.FromHexString(hexString); + return Encoding.ASCII.GetString(bytes); + } + + private string ToHex(byte[] bytes) + { + return string.Join("", bytes.Select(x => $"{x:x2}")); + } + + private string ToHex(string str) + { + return ToHex(Encoding.ASCII.GetBytes(str)); + } + + private string ToBinaryFormat(byte[] bytes) + { + return string.Join("", bytes.Select(x => + x switch + { + (byte)'#' => "}\x03", + (byte)'$' => "}\x04", + (byte)'*' => "}\x0a", + (byte)'}' => "}\x5d", + _ => Convert.ToChar(x).ToString(), + } + )); + } + + private string ToBinaryFormat(string str) + { + return ToBinaryFormat(Encoding.ASCII.GetBytes(str)); + } + + public void Dispose() + { + Dispose(true); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _shuttingDown = true; + + ListenerSocket.Stop(); + ClientSocket?.Shutdown(SocketShutdown.Both); + ClientSocket?.Close(); + ReadStream?.Close(); + WriteStream?.Close(); + DebuggerThread.Join(); + Messages.Add(new KillMessage()); + MessageHandlerThread.Join(); + Messages.Dispose(); + _breakHandlerEvent.Dispose(); + } + } + + public void BreakHandler(IExecutionContext ctx, ulong address, int imm) + { + DebugProcess.DebugInterruptHandler(ctx); + + _breakHandlerEvent.Reset(); + Messages.Add(new ThreadBreakMessage(ctx, address, imm)); + // Messages.Add can block, so we log it after adding the message to make sure user can see the log at the same time GDB receives the break message + Logger.Notice.Print(LogClass.GdbStub, $"Break hit on thread {ctx.ThreadUid} at pc {address:x016}"); + // Wait for the process to stop before returning to avoid BreakHander being called multiple times from the same breakpoint + _breakHandlerEvent.Wait(5000); + } + + public void StepHandler(IExecutionContext ctx) + { + DebugProcess.DebugInterruptHandler(ctx); + } + } +} diff --git a/src/Ryujinx.HLE/Debugger/GdbSignal.cs b/src/Ryujinx.HLE/Debugger/GdbSignal.cs new file mode 100644 index 000000000..ee4efbda4 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/GdbSignal.cs @@ -0,0 +1,15 @@ +namespace Ryujinx.HLE.Debugger +{ + enum GdbSignal + { + Zero = 0, + Int = 2, + Quit = 3, + Trap = 5, + Abort = 6, + Alarm = 14, + IO = 23, + XCPU = 24, + Unknown = 143 + } +} diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-core.xml b/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-core.xml new file mode 100644 index 000000000..9899a0e4a --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-core.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-fpu.xml b/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-fpu.xml new file mode 100644 index 000000000..a09120bc4 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/GdbXml/aarch64-fpu.xml @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/arm-core.xml b/src/Ryujinx.HLE/Debugger/GdbXml/arm-core.xml new file mode 100644 index 000000000..2307d65f9 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/GdbXml/arm-core.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/arm-neon.xml b/src/Ryujinx.HLE/Debugger/GdbXml/arm-neon.xml new file mode 100644 index 000000000..d61f6b854 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/GdbXml/arm-neon.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/target32.xml b/src/Ryujinx.HLE/Debugger/GdbXml/target32.xml new file mode 100644 index 000000000..890679858 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/GdbXml/target32.xml @@ -0,0 +1,14 @@ + + + + + + arm + + + diff --git a/src/Ryujinx.HLE/Debugger/GdbXml/target64.xml b/src/Ryujinx.HLE/Debugger/GdbXml/target64.xml new file mode 100644 index 000000000..cfd5bf780 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/GdbXml/target64.xml @@ -0,0 +1,14 @@ + + + + + + aarch64 + + + diff --git a/src/Ryujinx.HLE/Debugger/IDebuggableProcess.cs b/src/Ryujinx.HLE/Debugger/IDebuggableProcess.cs new file mode 100644 index 000000000..0896f25d2 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/IDebuggableProcess.cs @@ -0,0 +1,21 @@ +using Ryujinx.Cpu; +using Ryujinx.HLE.HOS.Kernel.Threading; +using Ryujinx.Memory; + +namespace Ryujinx.HLE.Debugger +{ + internal interface IDebuggableProcess + { + void DebugStop(); + void DebugContinue(); + void DebugContinue(KThread thread); + bool DebugStep(KThread thread); + KThread GetThread(ulong threadUid); + DebugState GetDebugState(); + bool IsThreadPaused(KThread thread); + ulong[] GetThreadUids(); + public void DebugInterruptHandler(IExecutionContext ctx); + IVirtualMemoryManager CpuMemory { get; } + void InvalidateCacheRegion(ulong address, ulong size); + } +} diff --git a/src/Ryujinx.HLE/Debugger/Message/BreakInMessage.cs b/src/Ryujinx.HLE/Debugger/Message/BreakInMessage.cs new file mode 100644 index 000000000..81d8784ae --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Message/BreakInMessage.cs @@ -0,0 +1,6 @@ +namespace Ryujinx.HLE.Debugger +{ + struct BreakInMessage : IMessage + { + } +} diff --git a/src/Ryujinx.HLE/Debugger/Message/CommandMessage.cs b/src/Ryujinx.HLE/Debugger/Message/CommandMessage.cs new file mode 100644 index 000000000..ad265d432 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Message/CommandMessage.cs @@ -0,0 +1,12 @@ +namespace Ryujinx.HLE.Debugger +{ + struct CommandMessage : IMessage + { + public string Command; + + public CommandMessage(string cmd) + { + Command = cmd; + } + } +} diff --git a/src/Ryujinx.HLE/Debugger/Message/IMessage.cs b/src/Ryujinx.HLE/Debugger/Message/IMessage.cs new file mode 100644 index 000000000..4b03183c5 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Message/IMessage.cs @@ -0,0 +1,6 @@ +namespace Ryujinx.HLE.Debugger +{ + interface IMessage + { + } +} diff --git a/src/Ryujinx.HLE/Debugger/Message/KillMessage.cs b/src/Ryujinx.HLE/Debugger/Message/KillMessage.cs new file mode 100644 index 000000000..43ae0f21e --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Message/KillMessage.cs @@ -0,0 +1,6 @@ +namespace Ryujinx.HLE.Debugger +{ + struct KillMessage : IMessage + { + } +} diff --git a/src/Ryujinx.HLE/Debugger/Message/SendNackMessage.cs b/src/Ryujinx.HLE/Debugger/Message/SendNackMessage.cs new file mode 100644 index 000000000..ce804c46e --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Message/SendNackMessage.cs @@ -0,0 +1,6 @@ +namespace Ryujinx.HLE.Debugger +{ + struct SendNackMessage : IMessage + { + } +} diff --git a/src/Ryujinx.HLE/Debugger/Message/ThreadBreakMessage.cs b/src/Ryujinx.HLE/Debugger/Message/ThreadBreakMessage.cs new file mode 100644 index 000000000..027096eeb --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/Message/ThreadBreakMessage.cs @@ -0,0 +1,18 @@ +using IExecutionContext = Ryujinx.Cpu.IExecutionContext; + +namespace Ryujinx.HLE.Debugger +{ + public class ThreadBreakMessage : IMessage + { + public IExecutionContext Context { get; } + public ulong Address { get; } + public int Opcode { get; } + + public ThreadBreakMessage(IExecutionContext context, ulong address, int opcode) + { + Context = context; + Address = address; + Opcode = opcode; + } + } +} diff --git a/src/Ryujinx.HLE/Debugger/RegisterInformation.cs b/src/Ryujinx.HLE/Debugger/RegisterInformation.cs new file mode 100644 index 000000000..b5fd88ea5 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/RegisterInformation.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.IO; + +namespace Ryujinx.HLE.Debugger +{ + class RegisterInformation + { + public static readonly Dictionary Features = new() + { + { "target64.xml", GetEmbeddedResourceContent("target64.xml") }, + { "target32.xml", GetEmbeddedResourceContent("target32.xml") }, + { "aarch64-core.xml", GetEmbeddedResourceContent("aarch64-core.xml") }, + { "aarch64-fpu.xml", GetEmbeddedResourceContent("aarch64-fpu.xml") }, + { "arm-core.xml", GetEmbeddedResourceContent("arm-core.xml") }, + { "arm-neon.xml", GetEmbeddedResourceContent("arm-neon.xml") }, + }; + + private static string GetEmbeddedResourceContent(string resourceName) + { + Stream stream = System.Reflection.Assembly.GetExecutingAssembly().GetManifestResourceStream("Ryujinx.HLE.Debugger.GdbXml." + resourceName); + StreamReader reader = new StreamReader(stream); + string result = reader.ReadToEnd(); + reader.Dispose(); + stream.Dispose(); + return result; + } + } +} diff --git a/src/Ryujinx.HLE/Debugger/StringStream.cs b/src/Ryujinx.HLE/Debugger/StringStream.cs new file mode 100644 index 000000000..d8148a9c2 --- /dev/null +++ b/src/Ryujinx.HLE/Debugger/StringStream.cs @@ -0,0 +1,109 @@ +using System.Diagnostics; +using System.Globalization; + +namespace Ryujinx.HLE.Debugger +{ + class StringStream + { + private readonly string Data; + private int Position; + + public StringStream(string s) + { + Data = s; + } + + public char ReadChar() + { + return Data[Position++]; + } + + public string ReadUntil(char needle) + { + int needlePos = Data.IndexOf(needle, Position); + + if (needlePos == -1) + { + needlePos = Data.Length; + } + + string result = Data.Substring(Position, needlePos - Position); + Position = needlePos + 1; + return result; + } + + public string ReadLength(int len) + { + string result = Data.Substring(Position, len); + Position += len; + return result; + } + + public string ReadRemaining() + { + string result = Data.Substring(Position); + Position = Data.Length; + return result; + } + + public ulong ReadRemainingAsHex() + { + return ulong.Parse(ReadRemaining(), NumberStyles.HexNumber); + } + + public ulong ReadUntilAsHex(char needle) + { + return ulong.Parse(ReadUntil(needle), NumberStyles.HexNumber); + } + + public ulong ReadLengthAsHex(int len) + { + return ulong.Parse(ReadLength(len), NumberStyles.HexNumber); + } + + public ulong ReadLengthAsLEHex(int len) + { + Debug.Assert(len % 2 == 0); + + ulong result = 0; + int pos = 0; + while (pos < len) + { + result += ReadLengthAsHex(2) << (4 * pos); + pos += 2; + } + return result; + } + + public ulong? ReadRemainingAsThreadUid() + { + string s = ReadRemaining(); + return s == "-1" ? null : ulong.Parse(s, NumberStyles.HexNumber); + } + + public bool ConsumePrefix(string prefix) + { + if (Data.Substring(Position).StartsWith(prefix)) + { + Position += prefix.Length; + return true; + } + return false; + } + + public bool ConsumeRemaining(string match) + { + if (Data.Substring(Position) == match) + { + Position += match.Length; + return true; + } + return false; + } + + public bool IsEmpty() + { + return Position >= Data.Length; + } + } +} diff --git a/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs b/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs index 759780c42..28f7ef25f 100644 --- a/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs +++ b/src/Ryujinx.HLE/HOS/ArmProcessContextFactory.cs @@ -69,7 +69,7 @@ namespace Ryujinx.HLE.HOS mode = MemoryManagerMode.SoftwarePageTable; } - ICpuEngine cpuEngine = isArm64Host && (mode == MemoryManagerMode.HostMapped || mode == MemoryManagerMode.HostMappedUnsafe) + ICpuEngine cpuEngine = isArm64Host && (mode == MemoryManagerMode.HostMapped || mode == MemoryManagerMode.HostMappedUnsafe) && !context.Device.Configuration.EnableGdbStub ? new LightningJitEngine(_tickSource) : new JitEngine(_tickSource); diff --git a/src/Ryujinx.HLE/HOS/Horizon.cs b/src/Ryujinx.HLE/HOS/Horizon.cs index 5063b4329..517f8ef16 100644 --- a/src/Ryujinx.HLE/HOS/Horizon.cs +++ b/src/Ryujinx.HLE/HOS/Horizon.cs @@ -5,6 +5,7 @@ using LibHac.Fs.Shim; using LibHac.FsSystem; using LibHac.Tools.FsSystem; using Ryujinx.Cpu; +using Ryujinx.HLE.Debugger; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.Kernel; using Ryujinx.HLE.HOS.Kernel.Memory; @@ -500,5 +501,21 @@ namespace Ryujinx.HLE.HOS IsPaused = pause; } + + internal IDebuggableProcess DebugGetApplicationProcessDebugInterface() + { + lock (KernelContext.Processes) + { + return KernelContext.Processes.Values.FirstOrDefault(x => x.IsApplication)?.DebugInterface; + } + } + + internal KProcess DebugGetApplicationProcess() + { + lock (KernelContext.Processes) + { + return KernelContext.Processes.Values.FirstOrDefault(x => x.IsApplication); + } + } } } diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs index c4a9835cc..87da9f7a6 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Process/HleProcessDebugger.cs @@ -4,6 +4,7 @@ using Ryujinx.HLE.HOS.Kernel.Memory; using Ryujinx.HLE.HOS.Kernel.Threading; using Ryujinx.HLE.Loaders.Elf; using Ryujinx.Memory; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -17,7 +18,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process private readonly KProcess _owner; - private class Image + public class Image { public ulong BaseAddress { get; } public ulong Size { get; } @@ -54,6 +55,15 @@ namespace Ryujinx.HLE.HOS.Kernel.Process trace.AppendLine($"Process: {_owner.Name}, PID: {_owner.Pid}"); + string ThreadName = thread.GetThreadName(); + + if (!String.IsNullOrEmpty(ThreadName)) + { + trace.AppendLine($"Thread ID: {thread.ThreadUid} ({ThreadName})"); + } else { + trace.AppendLine($"Thread ID: {thread.ThreadUid}"); + } + void AppendTrace(ulong address) { if (AnalyzePointer(out PointerInfo info, address, thread)) @@ -283,7 +293,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process return null; } - private string GetGuessedNsoNameFromIndex(int index) + public string GetGuessedNsoNameFromIndex(int index) { if ((uint)index > 11) { @@ -316,6 +326,16 @@ namespace Ryujinx.HLE.HOS.Kernel.Process } } + public List GetLoadedImages() + { + EnsureLoaded(); + + lock (_images) + { + return [.. _images]; + } + } + private void EnsureLoaded() { if (Interlocked.CompareExchange(ref _loaded, 1, 0) == 0) diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs index 478b4e864..0a57f5bc6 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Process/KProcess.cs @@ -1,6 +1,7 @@ using Ryujinx.Common; using Ryujinx.Common.Logging; using Ryujinx.Cpu; +using Ryujinx.HLE.Debugger; using Ryujinx.HLE.Exceptions; using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Memory; @@ -11,6 +12,8 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; +using ExceptionCallback = Ryujinx.Cpu.ExceptionCallback; +using ExceptionCallbackNoArgs = Ryujinx.Cpu.ExceptionCallbackNoArgs; namespace Ryujinx.HLE.HOS.Kernel.Process { @@ -89,6 +92,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Process public IVirtualMemoryManager CpuMemory => Context.AddressSpace; public HleProcessDebugger Debugger { get; private set; } + public IDebuggableProcess DebugInterface { get; private set; } + protected int debugState = (int)DebugState.Running; public KProcess(KernelContext context, bool allowCodeMemoryForJit = false) : base(context) { @@ -110,6 +115,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Process _threads = []; Debugger = new HleProcessDebugger(this); + DebugInterface = new DebuggerInterface(this); } public Result InitializeKip( @@ -679,6 +685,13 @@ namespace Ryujinx.HLE.HOS.Kernel.Process SetState(newState); + if (KernelContext.Device.Configuration.DebuggerSuspendOnStart && IsApplication) + { + mainThread.Suspend(ThreadSchedState.ThreadPauseFlag); + debugState = (int)DebugState.Stopped; + Logger.Notice.Print(LogClass.Kernel, $"Application is suspended on start for debugging."); + } + result = mainThread.Start(); if (result != Result.Success) @@ -727,9 +740,19 @@ namespace Ryujinx.HLE.HOS.Kernel.Process public IExecutionContext CreateExecutionContext() { + ExceptionCallback breakCallback = null; + ExceptionCallbackNoArgs stepCallback = null; + + if (KernelContext.Device.Configuration.EnableGdbStub && KernelContext.Device.Debugger != null) + { + breakCallback = KernelContext.Device.Debugger.BreakHandler; + stepCallback = KernelContext.Device.Debugger.StepHandler; + } + return Context?.CreateExecutionContext(new ExceptionCallbacks( InterruptHandler, - null, + breakCallback, + stepCallback, KernelContext.SyscallHandler.SvcCall, UndefinedInstructionHandler)); } @@ -1174,5 +1197,186 @@ namespace Ryujinx.HLE.HOS.Kernel.Process { return Capabilities.IsSvcPermitted(svcId); } + + private class DebuggerInterface : IDebuggableProcess + { + private Barrier StepBarrier; + private readonly KProcess _parent; + private readonly KernelContext _kernelContext; + private KThread steppingThread; + + public DebuggerInterface(KProcess p) + { + _parent = p; + _kernelContext = p.KernelContext; + StepBarrier = new(2); + } + + public void DebugStop() + { + if (Interlocked.CompareExchange(ref _parent.debugState, (int)DebugState.Stopping, + (int)DebugState.Running) != (int)DebugState.Running) + { + return; + } + + _kernelContext.CriticalSection.Enter(); + lock (_parent._threadingLock) + { + foreach (KThread thread in _parent._threads) + { + thread.Suspend(ThreadSchedState.ThreadPauseFlag); + thread.Context.RequestInterrupt(); + if (!thread.DebugHalt.WaitOne(TimeSpan.FromMilliseconds(50))) + { + Logger.Warning?.Print(LogClass.Kernel, $"Failed to suspend thread {thread.ThreadUid} in time."); + } + } + } + + _parent.debugState = (int)DebugState.Stopped; + _kernelContext.CriticalSection.Leave(); + } + + public void DebugContinue() + { + if (Interlocked.CompareExchange(ref _parent.debugState, (int)DebugState.Running, + (int)DebugState.Stopped) != (int)DebugState.Stopped) + { + return; + } + + _kernelContext.CriticalSection.Enter(); + lock (_parent._threadingLock) + { + foreach (KThread thread in _parent._threads) + { + thread.Resume(ThreadSchedState.ThreadPauseFlag); + } + } + _kernelContext.CriticalSection.Leave(); + } + + public void DebugContinue(KThread target) + { + Interlocked.Exchange(ref _parent.debugState, (int)DebugState.Running); + + _kernelContext.CriticalSection.Enter(); + lock (_parent._threadingLock) + { + target.Resume(ThreadSchedState.ThreadPauseFlag); + } + _kernelContext.CriticalSection.Leave(); + } + + public bool DebugStep(KThread target) + { + if (!IsThreadPaused(target)) + { + return false; + } + + _kernelContext.CriticalSection.Enter(); + steppingThread = target; + bool waiting = target.MutexOwner != null || target.WaitingSync || target.WaitingInArbitration; + target.Context.RequestDebugStep(); + if (waiting) + { + lock (_parent._threadingLock) + { + foreach (KThread thread in _parent._threads) + { + thread.Resume(ThreadSchedState.ThreadPauseFlag); + } + } + } + else + { + target.Resume(ThreadSchedState.ThreadPauseFlag); + } + _kernelContext.CriticalSection.Leave(); + + bool stepTimedOut = false; + if (!StepBarrier.SignalAndWait(TimeSpan.FromMilliseconds(2000))) + { + Logger.Warning?.Print(LogClass.Kernel, $"Failed to step thread {target.ThreadUid} in time."); + stepTimedOut = true; + } + + _kernelContext.CriticalSection.Enter(); + steppingThread = null; + if (waiting) + { + lock (_parent._threadingLock) + { + foreach (KThread thread in _parent._threads) + { + thread.Suspend(ThreadSchedState.ThreadPauseFlag); + } + } + } + else + { + target.Suspend(ThreadSchedState.ThreadPauseFlag); + } + _kernelContext.CriticalSection.Leave(); + + if (stepTimedOut) + { + return false; + } + + StepBarrier.SignalAndWait(); + return true; + } + + public DebugState GetDebugState() + { + return (DebugState)_parent.debugState; + } + + public bool IsThreadPaused(KThread target) + { + return (target.SchedFlags & ThreadSchedState.ThreadPauseFlag) != 0; + } + + public ulong[] GetThreadUids() + { + lock (_parent._threadingLock) + { + var threads = _parent._threads.Where(x => !x.TerminationRequested).ToArray(); + return threads.Select(x => x.ThreadUid).ToArray(); + } + } + + public KThread GetThread(ulong threadUid) + { + lock (_parent._threadingLock) + { + var threads = _parent._threads.Where(x => !x.TerminationRequested).ToArray(); + return threads.FirstOrDefault(x => x.ThreadUid == threadUid); + } + } + + public void DebugInterruptHandler(IExecutionContext ctx) + { + _kernelContext.CriticalSection.Enter(); + bool stepping = steppingThread != null; + _kernelContext.CriticalSection.Leave(); + if (stepping) + { + StepBarrier.SignalAndWait(); + StepBarrier.SignalAndWait(); + } + _parent.InterruptHandler(ctx); + } + + public IVirtualMemoryManager CpuMemory { get { return _parent.CpuMemory; } } + + public void InvalidateCacheRegion(ulong address, ulong size) + { + _parent.Context.InvalidateCacheRegion(address, size); + } + } } } diff --git a/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessExecutionContext.cs b/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessExecutionContext.cs index b8118fbb4..f0e44c4b7 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessExecutionContext.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Process/ProcessExecutionContext.cs @@ -1,5 +1,6 @@ using ARMeilleure.State; using Ryujinx.Cpu; +using System.Threading; namespace Ryujinx.HLE.HOS.Kernel.Process { @@ -17,10 +18,14 @@ namespace Ryujinx.HLE.HOS.Kernel.Process public bool IsAarch32 { get => false; set { } } + public ulong ThreadUid { get; set; } + public bool Running { get; private set; } = true; private readonly ulong[] _x = new ulong[32]; + public ulong DebugPc { get; set; } + public ulong GetX(int index) => _x[index]; public void SetX(int index, ulong value) => _x[index] = value; @@ -31,6 +36,10 @@ namespace Ryujinx.HLE.HOS.Kernel.Process { } + public void RequestDebugStep() + { + } + public void StopRunning() { Running = false; diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs index 7471702c3..54b20ff99 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KScheduler.cs @@ -301,6 +301,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading currentThread.SchedulerWaitEvent.Reset(); currentThread.ThreadContext.Unlock(); + currentThread.DebugHalt.Set(); // Wake all the threads that might be waiting until this thread context is unlocked. for (int core = 0; core < CpuCoresCount; core++) diff --git a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs index b5a14ad5b..20fb426ba 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Threading/KThread.cs @@ -1,12 +1,15 @@ using Ryujinx.Common.Logging; using Ryujinx.Cpu; +using Ryujinx.HLE.Debugger; using Ryujinx.HLE.HOS.Kernel.Common; using Ryujinx.HLE.HOS.Kernel.Process; using Ryujinx.HLE.HOS.Kernel.SupervisorCall; using Ryujinx.Horizon.Common; +using Ryujinx.Memory; using System; using System.Collections.Generic; using System.Numerics; +using System.Text; using System.Threading; namespace Ryujinx.HLE.HOS.Kernel.Threading @@ -16,6 +19,23 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading private const int TlsUserDisableCountOffset = 0x100; private const int TlsUserInterruptFlagOffset = 0x102; + // Tls -> ThreadType + private const int TlsThreadTypeOffsetAArch64 = 0x1F8; + private const int TlsThreadTypeOffsetAArch32 = 0x1FC; + + // Tls -> ThreadType -> Version + private const int TlsThreadTypeVersionOffsetAArch64 = 0x46; + private const int TlsThreadTypeVersionOffsetAArch32 = 0x26; + + // Tls -> ThreadType (Version 0) -> ThreadNamePointer + private const int TlsThreadTypeVersion0ThreadNamePointerOffsetAArch64 = 0x1A8; + private const int TlsThreadTypeVersion0ThreadNamePointerOffsetAArch32 = 0xE8; + + // Tls -> ThreadType (Version 1) -> ThreadNamePointer + private const int TlsThreadTypeThreadNamePointerOffsetAArch64 = 0x1A0; + private const int TlsThreadTypeThreadNamePointerOffsetAArch32 = 0xE4; + + public const int MaxWaitSyncObjects = 64; private ManualResetEvent _schedulerWaitEvent; @@ -114,6 +134,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading private readonly Lock _activityOperationLock = new(); + internal readonly ManualResetEvent DebugHalt = new(false); + public KThread(KernelContext context) : base(context) { WaitSyncObjects = new KSynchronizationObject[MaxWaitSyncObjects]; @@ -202,8 +224,10 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading } Context.TpidrroEl0 = (long)_tlsAddress; + Context.DebugPc = _entrypoint; ThreadUid = KernelContext.NewThreadUid(); + Context.ThreadUid = ThreadUid; HostThread.Name = customThreadStart != null ? $"HLE.OsThread.{ThreadUid}" : $"HLE.GuestThread.{ThreadUid}"; @@ -307,7 +331,9 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading { KernelContext.CriticalSection.Enter(); - if (Owner != null && Owner.PinnedThreads[KernelStatic.GetCurrentThread().CurrentCore] == this) + KThread currentThread = KernelStatic.GetCurrentThread(); + + if (Owner != null && currentThread != null && Owner.PinnedThreads[currentThread.CurrentCore] == this) { Owner.UnpinThread(this); } @@ -362,7 +388,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading { ThreadSchedState state = PrepareForTermination(); - if (state != ThreadSchedState.TerminationPending) + if (KernelStatic.GetCurrentThread() == this && state != ThreadSchedState.TerminationPending) { KernelContext.Synchronization.WaitFor(new KSynchronizationObject[] { this }, -1, out _); } @@ -1248,6 +1274,7 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading private void ThreadStart() { _schedulerWaitEvent.WaitOne(); + DebugHalt.Reset(); KernelStatic.SetKernelContext(KernelContext, this); if (_customThreadStart != null) @@ -1431,5 +1458,84 @@ namespace Ryujinx.HLE.HOS.Kernel.Threading { Owner.CpuMemory.Write(_tlsAddress + TlsUserInterruptFlagOffset, 0); } + + public string GetThreadName() + { + try + { + ulong threadNamePtr = 0; + if (Context.IsAarch32) + { + uint threadTypePtr32 = Owner.CpuMemory.Read(_tlsAddress + TlsThreadTypeOffsetAArch32); + if (threadTypePtr32 == 0) + { + return ""; + } + + ushort version = Owner.CpuMemory.Read(threadTypePtr32 + TlsThreadTypeVersionOffsetAArch32); + switch (version) + { + case 0x0000: + case 0xFFFF: + threadNamePtr = Owner.CpuMemory.Read(threadTypePtr32 + TlsThreadTypeVersion0ThreadNamePointerOffsetAArch32); + break; + case 0x0001: + threadNamePtr = Owner.CpuMemory.Read(threadTypePtr32 + TlsThreadTypeThreadNamePointerOffsetAArch32); + break; + default: + Logger.Warning?.Print(LogClass.Kernel, $"Unknown ThreadType struct version: {version}"); + break; + } + } + else + { + ulong threadTypePtr64 = Owner.CpuMemory.Read(_tlsAddress + TlsThreadTypeOffsetAArch64); + if (threadTypePtr64 == 0) + { + return ""; + } + + ushort version = Owner.CpuMemory.Read(threadTypePtr64 + TlsThreadTypeVersionOffsetAArch64); + switch (version) + { + case 0x0000: + case 0xFFFF: + threadNamePtr = Owner.CpuMemory.Read(threadTypePtr64 + TlsThreadTypeVersion0ThreadNamePointerOffsetAArch64); + break; + case 0x0001: + threadNamePtr = Owner.CpuMemory.Read(threadTypePtr64 + TlsThreadTypeThreadNamePointerOffsetAArch64); + break; + default: + Logger.Warning?.Print(LogClass.Kernel, $"Unknown ThreadType struct version: {version}"); + break; + } + } + + if (threadNamePtr == 0) + { + return ""; + } + + List nameBytes = new(); + for (int i = 0; i < 0x20; i++) + { + byte b = Owner.CpuMemory.Read(threadNamePtr + (ulong)i); + if (b == 0) + { + break; + } + nameBytes.Add(b); + } + return Encoding.UTF8.GetString(nameBytes.ToArray()); + + } catch (InvalidMemoryRegionException) + { + Logger.Warning?.Print(LogClass.Kernel, "Failed to get thread name."); + return ""; + } catch (Exception e) { + Logger.Error?.Print(LogClass.Kernel, $"Error getting thread name: {e.Message}"); + return ""; + } + } } } diff --git a/src/Ryujinx.HLE/HleConfiguration.cs b/src/Ryujinx.HLE/HleConfiguration.cs index 10c2a1f30..e2f95ede7 100644 --- a/src/Ryujinx.HLE/HleConfiguration.cs +++ b/src/Ryujinx.HLE/HleConfiguration.cs @@ -194,6 +194,21 @@ namespace Ryujinx.HLE /// public Action RefreshInputConfig { internal get; set; } + /// + /// Enables gdbstub to allow for debugging of the guest . + /// + public bool EnableGdbStub { internal get; set; } + + /// + /// A TCP port to use to expose a gdbstub for a debugger to connect to. + /// + public ushort GdbStubPort { internal get; set; } + + /// + /// Suspend execution when starting an application + /// + public bool DebuggerSuspendOnStart { internal get; set; } + /// /// The desired hacky workarounds. /// @@ -222,6 +237,9 @@ namespace Ryujinx.HLE bool multiplayerDisableP2p, string multiplayerLdnPassphrase, string multiplayerLdnServer, + bool enableGdbStub, + ushort gdbStubPort, + bool debuggerSuspendOnStart, int customVSyncInterval, EnabledDirtyHack[] dirtyHacks = null) { @@ -248,6 +266,9 @@ namespace Ryujinx.HLE MultiplayerDisableP2p = multiplayerDisableP2p; MultiplayerLdnPassphrase = multiplayerLdnPassphrase; MultiplayerLdnServer = multiplayerLdnServer; + EnableGdbStub = enableGdbStub; + GdbStubPort = gdbStubPort; + DebuggerSuspendOnStart = debuggerSuspendOnStart; Hacks = dirtyHacks ?? []; } diff --git a/src/Ryujinx.HLE/Ryujinx.HLE.csproj b/src/Ryujinx.HLE/Ryujinx.HLE.csproj index 5139d9276..1938796e8 100644 --- a/src/Ryujinx.HLE/Ryujinx.HLE.csproj +++ b/src/Ryujinx.HLE/Ryujinx.HLE.csproj @@ -33,6 +33,12 @@ + + + + + + @@ -42,6 +48,12 @@ + + + + + + diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index bdcbe82c7..e1aa8e0e4 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -14,6 +14,7 @@ using Ryujinx.HLE.Loaders.Processes; using Ryujinx.HLE.UI; using Ryujinx.Memory; using System; +using System.Threading; namespace Ryujinx.HLE { @@ -41,6 +42,7 @@ namespace Ryujinx.HLE public Hid Hid { get; } public TamperMachine TamperMachine { get; } public IHostUIHandler UIHandler { get; } + public Debugger.Debugger Debugger { get; } public int CpuCoresCount = 4; // Switch has a quad-core Tegra X1 SoC @@ -72,6 +74,7 @@ namespace Ryujinx.HLE AudioDeviceDriver = new CompatLayerHardwareDeviceDriver(Configuration.AudioDeviceDriver); Memory = new MemoryBlock(Configuration.MemoryConfiguration.ToDramSize(), memoryAllocationFlags); Gpu = new GpuContext(Configuration.GpuRenderer, DirtyHacks); + Debugger = Configuration.EnableGdbStub ? new Debugger.Debugger(this, Configuration.GdbStubPort) : null; System = new HOS.Horizon(this); Statistics = new PerformanceStatistics(this); Hid = new Hid(this, System.HidStorage); @@ -173,6 +176,7 @@ namespace Ryujinx.HLE AudioDeviceDriver.Dispose(); FileSystem.Dispose(); Memory.Dispose(); + Debugger?.Dispose(); TitleIDs.CurrentApplication.Value = null; Shared = null; diff --git a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs index f15d24e8a..dc7b8625e 100644 --- a/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs +++ b/src/Ryujinx/Headless/HeadlessRyujinx.Init.cs @@ -338,6 +338,9 @@ namespace Ryujinx.Headless false, string.Empty, string.Empty, + options.EnableGdbStub, + options.GdbStubPort, + options.DebuggerSuspendOnStart, options.CustomVSyncInterval ) .Configure( diff --git a/src/Ryujinx/Headless/Options.cs b/src/Ryujinx/Headless/Options.cs index 49050005c..876d0c936 100644 --- a/src/Ryujinx/Headless/Options.cs +++ b/src/Ryujinx/Headless/Options.cs @@ -423,6 +423,17 @@ namespace Ryujinx.Headless [Option("skip-user-profiles-manager", Required = false, Default = false, HelpText = "Enable skips the Profiles Manager popup during gameplay. Select the desired profile before starting the game")] public bool SkipUserProfilesManager { get; set; } + // Debug + + [Option("enable-gdb-stub", Required = false, Default = false, HelpText = "Enables the GDB stub so that a developer can attach a debugger to the emulated process.")] + public bool EnableGdbStub { get; set; } + + [Option("gdb-stub-port", Required = false, Default = 55555, HelpText = "Specifies which TCP port the GDB stub listens on.")] + public ushort GdbStubPort { get; set; } + + [Option("suspend-on-start", Required = false, Default = false, HelpText = "Suspend execution when starting an application.")] + public bool DebuggerSuspendOnStart { get; set; } + // Values [Value(0, MetaName = "input", HelpText = "Input to load.", Required = true)] diff --git a/src/Ryujinx/Systems/AppHost.cs b/src/Ryujinx/Systems/AppHost.cs index 1c5f64309..f11280d62 100644 --- a/src/Ryujinx/Systems/AppHost.cs +++ b/src/Ryujinx/Systems/AppHost.cs @@ -218,6 +218,10 @@ namespace Ryujinx.Ava.Systems ConfigurationState.Instance.Multiplayer.LdnServer.Event += UpdateLdnServerState; ConfigurationState.Instance.Multiplayer.DisableP2p.Event += UpdateDisableP2pState; + ConfigurationState.Instance.Debug.EnableGdbStub.Event += UpdateEnableGdbStubState; + ConfigurationState.Instance.Debug.GdbStubPort.Event += UpdateGdbStubPortState; + ConfigurationState.Instance.Debug.DebuggerSuspendOnStart.Event += UpdateDebuggerSuspendOnStartState; + _gpuCancellationTokenSource = new CancellationTokenSource(); _gpuDoneEvent = new ManualResetEvent(false); } @@ -564,6 +568,21 @@ namespace Ryujinx.Ava.Systems Device.Configuration.MultiplayerDisableP2p = e.NewValue; } + private void UpdateEnableGdbStubState(object sender, ReactiveEventArgs e) + { + Device.Configuration.EnableGdbStub = e.NewValue; + } + + private void UpdateGdbStubPortState(object sender, ReactiveEventArgs e) + { + Device.Configuration.GdbStubPort = e.NewValue; + } + + private void UpdateDebuggerSuspendOnStartState(object sender, ReactiveEventArgs e) + { + Device.Configuration.DebuggerSuspendOnStart = e.NewValue; + } + public void Stop() { _isActive = false; diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs index c21383349..26ea73f73 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationFileFormat.cs @@ -464,6 +464,21 @@ namespace Ryujinx.Ava.Systems.Configuration /// public bool UseHypervisor { get; set; } + /// + /// Enables or disables the GDB stub + /// + public bool EnableGdbStub { get; set; } + + /// + /// Which TCP port should the GDB stub listen on + /// + public ushort GdbStubPort { get; set; } + + /// + /// Suspend execution when starting an application + /// + public bool DebuggerSuspendOnStart { get; set; } + /// /// Show toggles for dirty hacks in the UI. /// diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs index afabdb4e3..57619aa70 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Migration.cs @@ -156,6 +156,10 @@ namespace Ryujinx.Ava.Systems.Configuration Multiplayer.LdnPassphrase.Value = cff.MultiplayerLdnPassphrase; Multiplayer.LdnServer.Value = cff.LdnServer; + Debug.EnableGdbStub.Value = shouldLoadFromFile ? cff.EnableGdbStub : Debug.EnableGdbStub.Value; // Get from global config only + Debug.GdbStubPort.Value = shouldLoadFromFile ? cff.GdbStubPort : Debug.GdbStubPort.Value; // Get from global config only + Debug.DebuggerSuspendOnStart.Value = shouldLoadFromFile ? cff.DebuggerSuspendOnStart : Debug.DebuggerSuspendOnStart.Value; // Get from global config only + { Hacks.ShowDirtyHacks.Value = shouldLoadFromFile ? cff.ShowDirtyHacks : Hacks.ShowDirtyHacks.Value; // Get from global config only diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs index 29a390b26..bc8fdb40a 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.Model.cs @@ -703,6 +703,37 @@ namespace Ryujinx.Ava.Systems.Configuration } } + /// + /// Debug configuration section + /// + public class DebugSection + { + /// + /// Enables or disables the GDB stub + /// + public ReactiveObject EnableGdbStub { get; private set; } + + /// + /// Which TCP port should the GDB stub listen on + /// + public ReactiveObject GdbStubPort { get; private set; } + + /// + /// Suspend execution when starting an application + /// + public ReactiveObject DebuggerSuspendOnStart { get; private set; } + + public DebugSection() + { + EnableGdbStub = new ReactiveObject(); + EnableGdbStub.LogChangesToValue(nameof(EnableGdbStub)); + GdbStubPort = new ReactiveObject(); + GdbStubPort.LogChangesToValue(nameof(GdbStubPort)); + DebuggerSuspendOnStart = new ReactiveObject(); + DebuggerSuspendOnStart.LogChangesToValue(nameof(DebuggerSuspendOnStart)); + } + } + public class HacksSection { /// @@ -801,6 +832,11 @@ namespace Ryujinx.Ava.Systems.Configuration /// public MultiplayerSection Multiplayer { get; private set; } + /// + /// The Debug + /// + public DebugSection Debug { get; private set; } + /// /// The Dirty Hacks section /// @@ -854,6 +890,7 @@ namespace Ryujinx.Ava.Systems.Configuration Graphics = new GraphicsSection(); Hid = new HidSection(); Multiplayer = new MultiplayerSection(); + Debug = new DebugSection(); Hacks = new HacksSection(); UpdateCheckerType = new ReactiveObject(); FocusLostActionType = new ReactiveObject(); @@ -893,6 +930,9 @@ namespace Ryujinx.Ava.Systems.Configuration Multiplayer.DisableP2p, Multiplayer.LdnPassphrase, Multiplayer.GetLdnServer(), + Debug.EnableGdbStub, + Debug.GdbStubPort, + Debug.DebuggerSuspendOnStart, Graphics.CustomVSyncInterval, Hacks.ShowDirtyHacks ? Hacks.EnabledHacks : null); } diff --git a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs index 4a565d5d3..185aedf64 100644 --- a/src/Ryujinx/Systems/Configuration/ConfigurationState.cs +++ b/src/Ryujinx/Systems/Configuration/ConfigurationState.cs @@ -147,6 +147,9 @@ namespace Ryujinx.Ava.Systems.Configuration MultiplayerDisableP2p = Multiplayer.DisableP2p, MultiplayerLdnPassphrase = Multiplayer.LdnPassphrase, LdnServer = Multiplayer.LdnServer, + EnableGdbStub = Debug.EnableGdbStub, + GdbStubPort = Debug.GdbStubPort, + DebuggerSuspendOnStart = Debug.DebuggerSuspendOnStart, ShowDirtyHacks = Hacks.ShowDirtyHacks, DirtyHacks = Hacks.EnabledHacks.Select(it => it.Pack()).ToArray(), }; @@ -324,6 +327,9 @@ namespace Ryujinx.Ava.Systems.Configuration }, } ]; + Debug.EnableGdbStub.Value = false; + Debug.GdbStubPort.Value = 55555; + Debug.DebuggerSuspendOnStart.Value = false; } private static GraphicsBackend DefaultGraphicsBackend() diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 654eb0c43..54fd951fb 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -71,6 +71,10 @@ namespace Ryujinx.Ava.UI.ViewModels private string _ldnPassphrase; [ObservableProperty] private string _ldnServer; + private bool _enableGDBStub; + private ushort _gdbStubPort; + private bool _debuggerSuspendOnStart; + public SettingsHacksViewModel DirtyHacks { get; } private readonly bool _isGameRunning; @@ -387,6 +391,36 @@ namespace Ryujinx.Ava.UI.ViewModels public bool IsInvalidLdnPassphraseVisible { get; set; } + public bool EnableGdbStub + { + get => _enableGDBStub; + set + { + _enableGDBStub = value; + ConfigurationState.Instance.Debug.EnableGdbStub.Value = _enableGDBStub; + } + } + + public ushort GDBStubPort + { + get => _gdbStubPort; + set + { + _gdbStubPort = value; + ConfigurationState.Instance.Debug.GdbStubPort.Value = _gdbStubPort; + } + } + + public bool DebuggerSuspendOnStart + { + get => _debuggerSuspendOnStart; + set + { + _debuggerSuspendOnStart = value; + ConfigurationState.Instance.Debug.DebuggerSuspendOnStart.Value = _debuggerSuspendOnStart; + } + } + public SettingsViewModel(VirtualFileSystem virtualFileSystem, ContentManager contentManager) : this() { _virtualFileSystem = virtualFileSystem; @@ -680,10 +714,16 @@ namespace Ryujinx.Ava.UI.ViewModels FsGlobalAccessLogMode = config.System.FsGlobalAccessLogMode; OpenglDebugLevel = (int)config.Logger.GraphicsDebugLevel.Value; + // Multiplayer MultiplayerModeIndex = (int)config.Multiplayer.Mode.Value; DisableP2P = config.Multiplayer.DisableP2p; LdnPassphrase = config.Multiplayer.LdnPassphrase; LdnServer = config.Multiplayer.LdnServer; + + // Debug + EnableGdbStub = config.Debug.EnableGdbStub.Value; + GDBStubPort = config.Debug.GdbStubPort.Value; + DebuggerSuspendOnStart = config.Debug.DebuggerSuspendOnStart.Value; } public void SaveSettings(bool global = false) @@ -800,12 +840,18 @@ namespace Ryujinx.Ava.UI.ViewModels config.System.FsGlobalAccessLogMode.Value = FsGlobalAccessLogMode; config.Logger.GraphicsDebugLevel.Value = (GraphicsDebugLevel)OpenglDebugLevel; + // Multiplayer config.Multiplayer.LanInterfaceId.Value = _networkInterfaces[NetworkInterfaceList[NetworkInterfaceIndex]]; config.Multiplayer.Mode.Value = (MultiplayerMode)MultiplayerModeIndex; config.Multiplayer.DisableP2p.Value = DisableP2P; config.Multiplayer.LdnPassphrase.Value = LdnPassphrase; config.Multiplayer.LdnServer.Value = LdnServer; + // Debug + config.Debug.EnableGdbStub.Value = EnableGdbStub; + config.Debug.GdbStubPort.Value = GDBStubPort; + config.Debug.DebuggerSuspendOnStart.Value = DebuggerSuspendOnStart; + // Dirty Hacks config.Hacks.Xc2MenuSoftlockFix.Value = DirtyHacks.Xc2MenuSoftlockFix; config.Hacks.DisableNifmIsAnyInternetRequestAccepted.Value = diff --git a/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml new file mode 100644 index 000000000..f491dda24 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml.cs new file mode 100644 index 000000000..14a65b8b2 --- /dev/null +++ b/src/Ryujinx/UI/Views/Settings/SettingsDebugView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; + +namespace Ryujinx.Ava.UI.Views.Settings +{ + public partial class SettingsDebugView : UserControl + { + public SettingsDebugView() + { + InitializeComponent(); + } + } +} + diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml b/src/Ryujinx/UI/Windows/SettingsWindow.axaml index 15d174123..9bfe0a9db 100644 --- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml +++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml @@ -46,6 +46,7 @@ + + Date: Tue, 5 Aug 2025 14:51:51 -0500 Subject: [PATCH 05/24] gdb: Improve stepping (ryubing/ryujinx!106) See merge request ryubing/ryujinx!106 --- .../Instructions/InstEmitFlowHelper.cs | 22 +++- .../Translation/ArmEmitterContext.cs | 5 +- src/ARMeilleure/Translation/Translator.cs | 105 +----------------- 3 files changed, 26 insertions(+), 106 deletions(-) diff --git a/src/ARMeilleure/Instructions/InstEmitFlowHelper.cs b/src/ARMeilleure/Instructions/InstEmitFlowHelper.cs index f67668da4..d0871b29f 100644 --- a/src/ARMeilleure/Instructions/InstEmitFlowHelper.cs +++ b/src/ARMeilleure/Instructions/InstEmitFlowHelper.cs @@ -143,6 +143,12 @@ namespace ARMeilleure.Instructions public static void EmitCall(ArmEmitterContext context, ulong immediate) { + if (context.IsSingleStep) + { + context.Return(Const(immediate)); + return; + } + bool isRecursive = immediate == context.EntryAddress; if (isRecursive) @@ -157,12 +163,24 @@ namespace ARMeilleure.Instructions public static void EmitVirtualCall(ArmEmitterContext context, Operand target) { - EmitTableBranch(context, target, isJump: false); + if (context.IsSingleStep) + { + if (target.Type == OperandType.I32) + { + target = context.ZeroExtend32(OperandType.I64, target); + } + + context.Return(target); + } + else + { + EmitTableBranch(context, target, isJump: false); + } } public static void EmitVirtualJump(ArmEmitterContext context, Operand target, bool isReturn) { - if (isReturn) + if (isReturn || context.IsSingleStep) { if (target.Type == OperandType.I32) { diff --git a/src/ARMeilleure/Translation/ArmEmitterContext.cs b/src/ARMeilleure/Translation/ArmEmitterContext.cs index 196120e92..85c3e6c50 100644 --- a/src/ARMeilleure/Translation/ArmEmitterContext.cs +++ b/src/ARMeilleure/Translation/ArmEmitterContext.cs @@ -52,6 +52,7 @@ namespace ARMeilleure.Translation public bool HighCq { get; } public bool HasPtc { get; } public Aarch32Mode Mode { get; } + public bool IsSingleStep { get; } private int _ifThenBlockStateIndex = 0; private Condition[] _ifThenBlockState = []; @@ -66,7 +67,8 @@ namespace ARMeilleure.Translation ulong entryAddress, bool highCq, bool hasPtc, - Aarch32Mode mode) + Aarch32Mode mode, + bool isSingleStep) { Memory = memory; CountTable = countTable; @@ -76,6 +78,7 @@ namespace ARMeilleure.Translation HighCq = highCq; HasPtc = hasPtc; Mode = mode; + IsSingleStep = isSingleStep; _labels = new Dictionary(); } diff --git a/src/ARMeilleure/Translation/Translator.cs b/src/ARMeilleure/Translation/Translator.cs index bbe641101..073b7ffe2 100644 --- a/src/ARMeilleure/Translation/Translator.cs +++ b/src/ARMeilleure/Translation/Translator.cs @@ -195,22 +195,6 @@ namespace ARMeilleure.Translation private ulong Step(State.ExecutionContext context, ulong address) { - try - { - OpCode opCode = Decoder.DecodeOpCode(Memory, address, context.ExecutionMode); - - // For branch instructions during single-stepping, we handle them manually - // func.Execute() will sometimes execute the entire function call, which is not what we want - if (opCode.Instruction.Name is InstName.Bl or InstName.Blr or InstName.Blx or InstName.Br) - { - return ExecuteBranchInstructionForStepping(context, address, opCode); - } - } - catch - { - // ignore - } - TranslatedFunction func = Translate(address, context.ExecutionMode, highCq: false, singleStep: true); address = func.Execute(Stubs.ContextWrapper, context); @@ -220,93 +204,7 @@ namespace ARMeilleure.Translation return address; } - private static ulong ExecuteBranchInstructionForStepping(State.ExecutionContext context, ulong address, OpCode opCode) - { - switch (opCode.Instruction.Name) - { - case InstName.Bl: - if (opCode is IOpCodeBImm opBImm) - { - // Set link register - if (context.ExecutionMode == ExecutionMode.Aarch64) - { - context.SetX(30, address + (ulong)opCode.OpCodeSizeInBytes); // LR = X30 - } - else - { - // For ARM32, need to set the appropriate return address - uint returnAddr = opCode is OpCode32 op32 && op32.IsThumb - ? (uint)address + (uint)opCode.OpCodeSizeInBytes | 1u // Thumb bit set - : (uint)address + (uint)opCode.OpCodeSizeInBytes; - context.SetX(14, returnAddr); // LR = R14 - } - return (ulong)opBImm.Immediate; - } - break; - case InstName.Blr: - if (opCode is OpCodeBReg opBReg) - { - // Set link register - if (context.ExecutionMode == ExecutionMode.Aarch64) - { - context.SetX(30, address + (ulong)opCode.OpCodeSizeInBytes); // LR = X30 - } - else - { - uint returnAddr = opCode is OpCode32 op32 && op32.IsThumb - ? (uint)address + (uint)opCode.OpCodeSizeInBytes | 1u // Thumb bit set - : (uint)address + (uint)opCode.OpCodeSizeInBytes; - context.SetX(14, returnAddr); // LR = R14 - } - return context.GetX(opBReg.Rn); - } - break; - - case InstName.Blx: - if (opCode is IOpCodeBImm opBlxImm) - { - // Handle mode switching for BLX - if (opCode is OpCode32 op32) - { - uint returnAddr = op32.IsThumb - ? (uint)address + (uint)opCode.OpCodeSizeInBytes | 1u - : (uint)address + (uint)opCode.OpCodeSizeInBytes; - context.SetX(14, returnAddr); - - // BLX switches between ARM and Thumb modes - context.SetPstateFlag(PState.TFlag, !op32.IsThumb); - } - return (ulong)opBlxImm.Immediate; - } - else if (opCode is IOpCode32BReg opBlxReg) - { - if (opCode is OpCode32 op32) - { - uint returnAddr = op32.IsThumb - ? (uint)address + (uint)opCode.OpCodeSizeInBytes | 1u - : (uint)address + (uint)opCode.OpCodeSizeInBytes; - context.SetX(14, returnAddr); - - // For BLX register, the target address determines the mode - ulong targetAddr = context.GetX(opBlxReg.Rm); - context.SetPstateFlag(PState.TFlag, (targetAddr & 1) != 0); - return targetAddr & ~1UL; // Clear the Thumb bit for the actual address - } - } - break; - - case InstName.Br: - if (opCode is OpCodeBReg opBr) - { - // BR doesn't set link register, just branches to the target - return context.GetX(opBr.Rn); - } - break; - } - - throw new InvalidOperationException($"Unhandled branch instruction: {opCode.Instruction.Name}"); - } internal TranslatedFunction GetOrTranslate(ulong address, ExecutionMode mode) { @@ -351,7 +249,8 @@ namespace ARMeilleure.Translation address, highCq, _ptc.State != PtcState.Disabled, - mode: Aarch32Mode.User); + mode: Aarch32Mode.User, + isSingleStep: singleStep); Logger.StartPass(PassName.Decoding); From 5613d3f35dd84147b71d86120301cd538511696c Mon Sep 17 00:00:00 2001 From: LotP <22-lotp@users.noreply.git.ryujinx.app> Date: Wed, 6 Aug 2025 15:57:08 -0500 Subject: [PATCH 06/24] Memory Changes (ryubing/ryujinx!46) See merge request ryubing/ryujinx!46 --- .../Collections/IntrusiveRedBlackTree.cs | 89 ++- .../Collections/IntrusiveRedBlackTreeNode.cs | 5 +- .../Collections/TreeDictionary.cs | 11 +- src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs | 49 +- .../Memory/BufferBackingState.cs | 6 +- .../Memory/BufferCache.cs | 191 +++--- .../Memory/BufferManager.cs | 12 +- .../Memory/BufferModifiedRangeList.cs | 485 ++++++++++------ .../Memory/BufferStage.cs | 2 + .../Memory/MemoryManager.cs | 7 +- .../Memory/VirtualRangeCache.cs | 66 +-- .../HOS/Kernel/Memory/KMemoryBlockManager.cs | 30 +- .../Range/INonOverlappingRange.cs | 2 +- .../Range/NonOverlappingRangeList.cs | 414 ++++++++++++- src/Ryujinx.Memory/Range/RangeList.cs | 548 ++++++++---------- src/Ryujinx.Memory/Range/RangeListBase.cs | 359 ++++++++++++ src/Ryujinx.Memory/Tracking/MemoryTracking.cs | 68 ++- 17 files changed, 1596 insertions(+), 748 deletions(-) create mode 100644 src/Ryujinx.Memory/Range/RangeListBase.cs diff --git a/src/Ryujinx.Common/Collections/IntrusiveRedBlackTree.cs b/src/Ryujinx.Common/Collections/IntrusiveRedBlackTree.cs index 423cd22c8..18e60687d 100644 --- a/src/Ryujinx.Common/Collections/IntrusiveRedBlackTree.cs +++ b/src/Ryujinx.Common/Collections/IntrusiveRedBlackTree.cs @@ -14,12 +14,13 @@ namespace Ryujinx.Common.Collections /// Adds a new node into the tree. /// /// Node to be added + /// Node to be added under /// is null - public void Add(T node) + public void Add(T node, T parent = null) { ArgumentNullException.ThrowIfNull(node); - Insert(node); + Insert(node, parent); } /// @@ -76,9 +77,11 @@ namespace Ryujinx.Common.Collections /// Inserts a new node into the tree. /// /// Node to be inserted - private void Insert(T node) + /// Node to be inserted under + private void Insert(T node, T parent = null) { - T newNode = BSTInsert(node); + T newNode = parent != null ? InsertWithParent(node, parent) : BSTInsert(node); + RestoreBalanceAfterInsertion(newNode); } @@ -122,10 +125,78 @@ namespace Ryujinx.Common.Collections else if (newNode.CompareTo(parent) < 0) { parent.Left = newNode; + + newNode.Successor = parent; + + if (parent.Predecessor != null) + { + newNode.Predecessor = parent.Predecessor; + parent.Predecessor = newNode; + newNode.Predecessor.Successor = newNode; + } + + parent.Predecessor = newNode; } else { parent.Right = newNode; + + newNode.Predecessor = parent; + + if (parent.Successor != null) + { + newNode.Successor = parent.Successor; + newNode.Successor.Predecessor = newNode; + } + + parent.Successor = newNode; + } + Count++; + return newNode; + } + + /// + /// Insertion Mechanism for a Binary Search Tree (BST). + ///

+ /// Inserts a new node directly under a parent node + /// where all children in the left subtree are less than , + /// and all children in the right subtree are greater than . + ///
+ /// Node to be inserted + /// Node to be inserted under + /// The inserted Node + private T InsertWithParent(T newNode, T parent) + { + newNode.Parent = parent; + + if (newNode.CompareTo(parent) < 0) + { + parent.Left = newNode; + + newNode.Successor = parent; + + if (parent.Predecessor != null) + { + newNode.Predecessor = parent.Predecessor; + parent.Predecessor = newNode; + newNode.Predecessor.Successor = newNode; + } + + parent.Predecessor = newNode; + } + else + { + parent.Right = newNode; + + newNode.Predecessor = parent; + + if (parent.Successor != null) + { + newNode.Successor = parent.Successor; + newNode.Successor.Predecessor = newNode; + } + + parent.Successor = newNode; } Count++; @@ -159,7 +230,7 @@ namespace Ryujinx.Common.Collections } else { - T element = Minimum(RightOf(nodeToDelete)); + T element = nodeToDelete.Successor; child = RightOf(element); parent = ParentOf(element); @@ -187,6 +258,9 @@ namespace Ryujinx.Common.Collections element.Left = old.Left; element.Right = old.Right; element.Parent = old.Parent; + element.Predecessor = old.Predecessor; + if (element.Predecessor != null) + element.Predecessor.Successor = element; if (ParentOf(old) == null) { @@ -241,6 +315,11 @@ namespace Ryujinx.Common.Collections { RestoreBalanceAfterRemoval(child); } + + if (old.Successor != null) + old.Successor.Predecessor = old.Predecessor; + if (old.Predecessor != null) + old.Predecessor.Successor = old.Successor; return old; } diff --git a/src/Ryujinx.Common/Collections/IntrusiveRedBlackTreeNode.cs b/src/Ryujinx.Common/Collections/IntrusiveRedBlackTreeNode.cs index 29d2d0c9a..57e0b27c8 100644 --- a/src/Ryujinx.Common/Collections/IntrusiveRedBlackTreeNode.cs +++ b/src/Ryujinx.Common/Collections/IntrusiveRedBlackTreeNode.cs @@ -9,8 +9,7 @@ namespace Ryujinx.Common.Collections public T Left; public T Right; public T Parent; - - public T Predecessor => IntrusiveRedBlackTreeImpl.PredecessorOf((T)this); - public T Successor => IntrusiveRedBlackTreeImpl.SuccessorOf((T)this); + public T Predecessor; + public T Successor; } } diff --git a/src/Ryujinx.Common/Collections/TreeDictionary.cs b/src/Ryujinx.Common/Collections/TreeDictionary.cs index af104d268..453f128d3 100644 --- a/src/Ryujinx.Common/Collections/TreeDictionary.cs +++ b/src/Ryujinx.Common/Collections/TreeDictionary.cs @@ -109,7 +109,7 @@ namespace Ryujinx.Common.Collections Node node = GetNode(key); if (node != null) { - Node successor = SuccessorOf(node); + Node successor = node.Successor; return successor != null ? successor.Key : default; } @@ -127,7 +127,7 @@ namespace Ryujinx.Common.Collections Node node = GetNode(key); if (node != null) { - Node predecessor = PredecessorOf(node); + Node predecessor = node.Predecessor; return predecessor != null ? predecessor.Key : default; } @@ -136,11 +136,10 @@ namespace Ryujinx.Common.Collections } /// - /// Adds all the nodes in the dictionary as key/value pairs into . + /// Adds all the nodes in the dictionary as key/value pairs into a list. ///

/// The key/value pairs will be added in Level Order. ///
- /// List to add the tree pairs into public List> AsLevelOrderList() { List> list = []; @@ -170,7 +169,7 @@ namespace Ryujinx.Common.Collections } /// - /// Adds all the nodes in the dictionary into . + /// Adds all the nodes in the dictionary into a list. /// /// A list of all KeyValuePairs sorted by Key Order public List> AsList() @@ -284,7 +283,7 @@ namespace Ryujinx.Common.Collections } Node newNode = new(key, value, parent); - if (newNode.Parent == null) + if (parent == null) { Root = newNode; } diff --git a/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs b/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs index a2448d76f..293f7fd2c 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/Buffer.cs @@ -15,7 +15,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Buffer, used to store vertex and index data, uniform and storage buffers, and others. /// - class Buffer : IRange, ISyncActionHandler, IDisposable + class Buffer : INonOverlappingRange, ISyncActionHandler, IDisposable { private const ulong GranularBufferThreshold = 4096; @@ -35,7 +35,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Size of the buffer in bytes. /// - public ulong Size { get; } + public ulong Size { get; private set; } /// /// End address of the buffer in guest memory. @@ -60,13 +60,13 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// This is null until at least one modification occurs. /// - private BufferModifiedRangeList _modifiedRanges = null; + private BufferModifiedRangeList _modifiedRanges; /// /// A structure that is used to flush buffer data back to a host mapped buffer for cached readback. /// Only used if the buffer data is explicitly owned by device local memory. /// - private BufferPreFlush _preFlush = null; + private BufferPreFlush _preFlush; /// /// Usage tracking state that determines what type of backing the buffer should use. @@ -110,7 +110,7 @@ namespace Ryujinx.Graphics.Gpu.Memory ulong size, BufferStage stage, bool sparseCompatible, - IEnumerable baseBuffers = null) + List baseBuffers) { _context = context; _physicalMemory = physicalMemory; @@ -126,21 +126,22 @@ namespace Ryujinx.Graphics.Gpu.Memory _useGranular = size > GranularBufferThreshold; - IEnumerable baseHandles = null; + List baseHandles = null; - if (baseBuffers != null) + if (baseBuffers.Count != 0) { - baseHandles = baseBuffers.SelectMany(buffer => + baseHandles = new List(); + foreach (Buffer buffer in baseBuffers) { if (buffer._useGranular) { - return buffer._memoryTrackingGranular.GetHandles(); + baseHandles.AddRange((buffer._memoryTrackingGranular.GetHandles())); } else { - return Enumerable.Repeat(buffer._memoryTracking, 1); + baseHandles.Add(buffer._memoryTracking); } - }); + } } if (_useGranular) @@ -171,9 +172,9 @@ namespace Ryujinx.Graphics.Gpu.Memory _memoryTracking.RegisterPreciseAction(PreciseAction); } - _externalFlushDelegate = new RegionSignal(ExternalFlush); - _loadDelegate = new Action(LoadRegion); - _modifiedDelegate = new Action(RegionModified); + _externalFlushDelegate = ExternalFlush; + _loadDelegate = LoadRegion; + _modifiedDelegate = RegionModified; _virtualDependenciesLock = new ReaderWriterLockSlim(); } @@ -247,6 +248,11 @@ namespace Ryujinx.Graphics.Gpu.Memory return Address < address + size && address < EndAddress; } + public INonOverlappingRange Split(ulong splitAddress) + { + throw new NotImplementedException(); + } + /// /// Checks if a given range is fully contained in the buffer. /// @@ -435,7 +441,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// The buffer to inherit from public void InheritModifiedRanges(Buffer from) { - if (from._modifiedRanges != null && from._modifiedRanges.HasRanges) + if (from._modifiedRanges is { HasRanges: true }) { if (from._syncActionRegistered && !_syncActionRegistered) { @@ -443,7 +449,7 @@ namespace Ryujinx.Graphics.Gpu.Memory _syncActionRegistered = true; } - void registerRangeAction(ulong address, ulong size) + void RegisterRangeAction(ulong address, ulong size) { if (_useGranular) { @@ -457,7 +463,7 @@ namespace Ryujinx.Graphics.Gpu.Memory EnsureRangeList(); - _modifiedRanges.InheritRanges(from._modifiedRanges, registerRangeAction); + _modifiedRanges.InheritRanges(from._modifiedRanges, RegisterRangeAction); } if (from._dirtyStart != ulong.MaxValue) @@ -499,14 +505,7 @@ namespace Ryujinx.Graphics.Gpu.Memory { // Cut off the start. - if (end < _dirtyEnd) - { - _dirtyStart = end; - } - else - { - _dirtyStart = ulong.MaxValue; - } + _dirtyStart = end < _dirtyEnd ? end : ulong.MaxValue; } else if (end >= _dirtyEnd) { diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs index a9b1f50e2..df130bb1d 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferBackingState.cs @@ -56,7 +56,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Parent buffer /// Initial buffer stage /// Buffers to inherit state from - public BufferBackingState(GpuContext context, Buffer parent, BufferStage stage, IEnumerable baseBuffers = null) + public BufferBackingState(GpuContext context, Buffer parent, BufferStage stage, List baseBuffers) { _size = (int)parent.Size; _systemMemoryType = context.Capabilities.MemoryType; @@ -72,7 +72,7 @@ namespace Ryujinx.Graphics.Gpu.Memory BufferStage storageFlags = stage & BufferStage.StorageMask; - if (parent.Size > DeviceLocalSizeThreshold && baseBuffers == null) + if (parent.Size > DeviceLocalSizeThreshold && baseBuffers.Count == 0) { _desiredType = BufferBackingType.DeviceMemory; } @@ -100,7 +100,7 @@ namespace Ryujinx.Graphics.Gpu.Memory // TODO: Might be nice to force atomic access to be device local for any stage. } - if (baseBuffers != null) + if (baseBuffers.Count != 0) { foreach (Buffer buffer in baseBuffers) { diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs index d02efcb29..8d0bf9833 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferCache.cs @@ -2,7 +2,6 @@ using Ryujinx.Graphics.GAL; using Ryujinx.Memory.Range; using System; using System.Collections.Generic; -using System.Linq; using System.Runtime.CompilerServices; namespace Ryujinx.Graphics.Gpu.Memory @@ -39,11 +38,9 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Only modified from the GPU thread. Must lock for add/remove. /// Must lock for any access from other threads. /// - private readonly RangeList _buffers; + private readonly NonOverlappingRangeList _buffers; private readonly MultiRangeList _multiRangeBuffers; - private Buffer[] _bufferOverlaps; - private readonly Dictionary _dirtyCache; private readonly Dictionary _modifiedCache; private bool _pruneCaches; @@ -64,8 +61,6 @@ namespace Ryujinx.Graphics.Gpu.Memory _buffers = []; _multiRangeBuffers = []; - _bufferOverlaps = new Buffer[OverlapsBufferInitialCapacity]; - _dirtyCache = new Dictionary(); // There are a lot more entries on the modified cache, so it is separate from the one for ForceDirty. @@ -79,24 +74,23 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Event arguments public void MemoryUnmappedHandler(object sender, UnmapEventArgs e) { - Buffer[] overlaps = new Buffer[10]; - int overlapCount; - MultiRange range = ((MemoryManager)sender).GetPhysicalRegions(e.Address, e.Size); for (int index = 0; index < range.Count; index++) { MemoryRange subRange = range.GetSubRange(index); + + _buffers.Lock.EnterReadLock(); + (RangeItem first, RangeItem last) = _buffers.FindOverlaps(subRange.Address, subRange.Size); - lock (_buffers) + RangeItem current = first; + while (last != null && current != last.Next) { - overlapCount = _buffers.FindOverlaps(subRange.Address, subRange.Size, ref overlaps); + current.Value.Unmapped(subRange.Address, subRange.Size); + current = current.Next; } - for (int i = 0; i < overlapCount; i++) - { - overlaps[i].Unmapped(subRange.Address, subRange.Size); - } + _buffers.Lock.ExitReadLock(); } } @@ -137,7 +131,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Physical ranges of the buffer, after address translation public MultiRange TranslateAndCreateMultiBuffers(MemoryManager memoryManager, ulong gpuVa, ulong size, BufferStage stage) { - if (gpuVa == 0) + if (gpuVa == 0 || size == 0) { return new MultiRange(MemoryManager.PteUnmapped, size); } @@ -336,7 +330,7 @@ namespace Ryujinx.Graphics.Gpu.Memory ulong alignedEndAddress = (endAddress + alignmentMask) & ~alignmentMask; ulong alignedSize = alignedEndAddress - alignedAddress; - Buffer buffer = _buffers.FindFirstOverlap(alignedAddress, alignedSize); + Buffer buffer = _buffers.FindOverlap(alignedAddress, alignedSize).Value; BufferRange bufferRange = buffer.GetRange(alignedAddress, alignedSize, false); alignedSubRanges[i] = new MemoryRange(alignedAddress, alignedSize); @@ -403,7 +397,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (subRange.Address != MemoryManager.PteUnmapped) { - Buffer buffer = _buffers.FindFirstOverlap(subRange.Address, subRange.Size); + Buffer buffer = _buffers.FindOverlap(subRange.Address, subRange.Size).Value; virtualBuffer.AddPhysicalDependency(buffer, subRange.Address, dstOffset, subRange.Size); physicalBuffers.Add(buffer); @@ -495,10 +489,10 @@ namespace Ryujinx.Graphics.Gpu.Memory /// The type of usage that created the buffer private void CreateBufferAligned(ulong address, ulong size, BufferStage stage) { - Buffer[] overlaps = _bufferOverlaps; - int overlapsCount = _buffers.FindOverlapsNonOverlapping(address, size, ref overlaps); + _buffers.Lock.EnterWriteLock(); + (RangeItem first, RangeItem last) = _buffers.FindOverlaps(address, size); - if (overlapsCount != 0) + if (first is not null) { // The buffer already exists. We can just return the existing buffer // if the buffer we need is fully contained inside the overlapping buffer. @@ -507,9 +501,8 @@ namespace Ryujinx.Graphics.Gpu.Memory // old buffer(s) to the new buffer. ulong endAddress = address + size; - Buffer overlap0 = overlaps[0]; - if (overlap0.Address > address || overlap0.EndAddress < endAddress) + if (first.Address > address || first.EndAddress < endAddress) { bool anySparseCompatible = false; @@ -522,53 +515,52 @@ namespace Ryujinx.Graphics.Gpu.Memory // sequential memory. // Allowing for 2 pages (rather than just one) is necessary to catch cases where the // range crosses a page, and after alignment, ends having a size of 2 pages. - if (overlapsCount == 1 && - address >= overlap0.Address && - endAddress - overlap0.EndAddress <= BufferAlignmentSize * 2) + if (first == last && + address >= first.Address && + endAddress - first.EndAddress <= BufferAlignmentSize * 2) { // Try to grow the buffer by 1.5x of its current size. // This improves performance in the cases where the buffer is resized often by small amounts. - ulong existingSize = overlap0.Size; + ulong existingSize = first.Value.Size; ulong growthSize = (existingSize + Math.Min(existingSize >> 1, MaxDynamicGrowthSize)) & ~BufferAlignmentMask; size = Math.Max(size, growthSize); endAddress = address + size; - overlapsCount = _buffers.FindOverlapsNonOverlapping(address, size, ref overlaps); + (first, last) = _buffers.FindOverlaps(address, size); } + + address = Math.Min(address, first.Address); + endAddress = Math.Max(endAddress, last.EndAddress); - for (int index = 0; index < overlapsCount; index++) + List overlaps = []; + + RangeItem current = first; + while (current != last.Next) { - Buffer buffer = overlaps[index]; - - anySparseCompatible |= buffer.SparseCompatible; - - address = Math.Min(address, buffer.Address); - endAddress = Math.Max(endAddress, buffer.EndAddress); - - lock (_buffers) - { - _buffers.Remove(buffer); - } + anySparseCompatible |= current.Value.SparseCompatible; + overlaps.Add(current.Value); + _buffers.Remove(current.Value); + + current = current.Next; } - + ulong newSize = endAddress - address; - CreateBufferAligned(address, newSize, stage, anySparseCompatible, overlaps, overlapsCount); + Buffer newBuffer = CreateBufferAligned(address, newSize, stage, anySparseCompatible, overlaps); + + _buffers.Add(newBuffer); } } else { // No overlap, just create a new buffer. - Buffer buffer = new(_context, _physicalMemory, address, size, stage, sparseCompatible: false); + Buffer buffer = new(_context, _physicalMemory, address, size, stage, sparseCompatible: false, []); - lock (_buffers) - { - _buffers.Add(buffer); - } + _buffers.Add(buffer); } - - ShrinkOverlapsBufferIfNeeded(); + + _buffers.Lock.ExitWriteLock(); } /// @@ -582,72 +574,68 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Alignment of the start address of the buffer private void CreateBufferAligned(ulong address, ulong size, BufferStage stage, ulong alignment) { - Buffer[] overlaps = _bufferOverlaps; - int overlapsCount = _buffers.FindOverlapsNonOverlapping(address, size, ref overlaps); bool sparseAligned = alignment >= SparseBufferAlignmentSize; + + _buffers.Lock.EnterWriteLock(); + (RangeItem first, RangeItem last) = _buffers.FindOverlaps(address, size); - if (overlapsCount != 0) + if (first is not null) { // If the buffer already exists, make sure if covers the entire range, // and make sure it is properly aligned, otherwise sparse mapping may fail. ulong endAddress = address + size; - Buffer overlap0 = overlaps[0]; - if (overlap0.Address > address || - overlap0.EndAddress < endAddress || - (overlap0.Address & (alignment - 1)) != 0 || - (!overlap0.SparseCompatible && sparseAligned)) + if (first.Address > address || + first.EndAddress < endAddress || + (first.Address & (alignment - 1)) != 0 || + (!first.Value.SparseCompatible && sparseAligned)) { // We need to make sure the new buffer is properly aligned. // However, after the range is aligned, it is possible that it // overlaps more buffers, so try again after each extension // and ensure we cover all overlaps. - int oldOverlapsCount; + RangeItem oldFirst; + endAddress = Math.Max(endAddress, last.EndAddress); do { - for (int index = 0; index < overlapsCount; index++) - { - Buffer buffer = overlaps[index]; - - address = Math.Min(address, buffer.Address); - endAddress = Math.Max(endAddress, buffer.EndAddress); - } + address = Math.Min(address, first.Address); address &= ~(alignment - 1); - oldOverlapsCount = overlapsCount; - overlapsCount = _buffers.FindOverlapsNonOverlapping(address, endAddress - address, ref overlaps); - } - while (oldOverlapsCount != overlapsCount); - - lock (_buffers) - { - for (int index = 0; index < overlapsCount; index++) - { - _buffers.Remove(overlaps[index]); - } + oldFirst = first; + (first, last) = _buffers.FindOverlaps(address, endAddress - address); } + while (oldFirst != first); ulong newSize = endAddress - address; - - CreateBufferAligned(address, newSize, stage, sparseAligned, overlaps, overlapsCount); + + List overlaps = []; + + RangeItem current = first; + while (current != last.Next) + { + overlaps.Add(current.Value); + _buffers.Remove(current.Value); + + current = current.Next; + } + + Buffer newBuffer = CreateBufferAligned(address, newSize, stage, sparseAligned, overlaps); + + _buffers.Add(newBuffer); } } else { // No overlap, just create a new buffer. - Buffer buffer = new(_context, _physicalMemory, address, size, stage, sparseAligned); + Buffer buffer = new(_context, _physicalMemory, address, size, stage, sparseAligned, []); - lock (_buffers) - { - _buffers.Add(buffer); - } + _buffers.Add(buffer); } - - ShrinkOverlapsBufferIfNeeded(); + _buffers.Lock.ExitWriteLock(); } /// @@ -660,17 +648,11 @@ namespace Ryujinx.Graphics.Gpu.Memory /// The type of usage that created the buffer /// Indicates if the buffer can be used in a sparse buffer mapping /// Buffers overlapping the range - /// Total of overlaps - private void CreateBufferAligned(ulong address, ulong size, BufferStage stage, bool sparseCompatible, Buffer[] overlaps, int overlapsCount) + private Buffer CreateBufferAligned(ulong address, ulong size, BufferStage stage, bool sparseCompatible, List overlaps) { - Buffer newBuffer = new(_context, _physicalMemory, address, size, stage, sparseCompatible, overlaps.Take(overlapsCount)); + Buffer newBuffer = new(_context, _physicalMemory, address, size, stage, sparseCompatible, overlaps); - lock (_buffers) - { - _buffers.Add(newBuffer); - } - - for (int index = 0; index < overlapsCount; index++) + for (int index = 0; index < overlaps.Count; index++) { Buffer buffer = overlaps[index]; @@ -688,6 +670,8 @@ namespace Ryujinx.Graphics.Gpu.Memory NotifyBuffersModified?.Invoke(); RecreateMultiRangeBuffers(address, size); + + return newBuffer; } /// @@ -718,17 +702,6 @@ namespace Ryujinx.Graphics.Gpu.Memory } } - /// - /// Resizes the temporary buffer used for range list intersection results, if it has grown too much. - /// - private void ShrinkOverlapsBufferIfNeeded() - { - if (_bufferOverlaps.Length > OverlapsBufferMaxCapacity) - { - Array.Resize(ref _bufferOverlaps, OverlapsBufferMaxCapacity); - } - } - /// /// Copy a buffer data from a given address to another. /// @@ -909,7 +882,7 @@ namespace Ryujinx.Graphics.Gpu.Memory { MemoryRange subRange = range.GetSubRange(i); - Buffer subBuffer = _buffers.FindFirstOverlap(subRange.Address, subRange.Size); + Buffer subBuffer = _buffers.FindOverlapFast(subRange.Address, subRange.Size).Value; subBuffer.SynchronizeMemory(subRange.Address, subRange.Size); @@ -957,7 +930,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (size != 0) { - buffer = _buffers.FindFirstOverlap(address, size); + buffer = _buffers.FindOverlapFast(address, size).Value; buffer.CopyFromDependantVirtualBuffers(); buffer.SynchronizeMemory(address, size); @@ -969,7 +942,7 @@ namespace Ryujinx.Graphics.Gpu.Memory } else { - buffer = _buffers.FindFirstOverlap(address, 1); + buffer = _buffers.FindOverlapFast(address, 1).Value; } return buffer; @@ -1007,7 +980,7 @@ namespace Ryujinx.Graphics.Gpu.Memory { if (size != 0) { - Buffer buffer = _buffers.FindFirstOverlap(address, size); + Buffer buffer = _buffers.FindOverlapFast(address, size).Value; if (copyBackVirtual) { diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs index cb99b455b..73647bef5 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferManager.cs @@ -258,7 +258,7 @@ namespace Ryujinx.Graphics.Gpu.Memory RecordStorageAlignment(_cpStorageBuffers, index, gpuVa); - gpuVa = BitUtils.AlignDown(gpuVa, (ulong)_context.Capabilities.StorageBufferOffsetAlignment); + gpuVa = BitUtils.AlignDown(gpuVa, (ulong)_context.Capabilities.StorageBufferOffsetAlignment); MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStageUtils.ComputeStorage(flags)); @@ -282,7 +282,7 @@ namespace Ryujinx.Graphics.Gpu.Memory RecordStorageAlignment(buffers, index, gpuVa); - gpuVa = BitUtils.AlignDown(gpuVa, (ulong)_context.Capabilities.StorageBufferOffsetAlignment); + gpuVa = BitUtils.AlignDown(gpuVa, (ulong)_context.Capabilities.StorageBufferOffsetAlignment); MultiRange range = _channel.MemoryManager.Physical.BufferCache.TranslateAndCreateMultiBuffers(_channel.MemoryManager, gpuVa, size, BufferStageUtils.GraphicsStorage(stage, flags)); @@ -761,7 +761,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (!bounds.IsUnmapped) { - bool isWrite = bounds.Flags.HasFlag(BufferUsageFlags.Write); + bool isWrite = (bounds.Flags & BufferUsageFlags.Write) == BufferUsageFlags.Write; BufferRange range = isStorage ? bufferCache.GetBufferRangeAligned(bounds.Range, bufferStage | BufferStageUtils.FromUsage(bounds.Flags), isWrite) : bufferCache.GetBufferRange(bounds.Range, bufferStage); @@ -798,7 +798,7 @@ namespace Ryujinx.Graphics.Gpu.Memory if (!bounds.IsUnmapped) { - bool isWrite = bounds.Flags.HasFlag(BufferUsageFlags.Write); + bool isWrite = (bounds.Flags & BufferUsageFlags.Write) == BufferUsageFlags.Write; BufferRange range = isStorage ? bufferCache.GetBufferRangeAligned(bounds.Range, BufferStageUtils.ComputeStorage(bounds.Flags), isWrite) : bufferCache.GetBufferRange(bounds.Range, BufferStage.Compute); @@ -817,7 +817,6 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Bind respective buffer bindings on the host API. /// /// Host buffers to bind, with their offsets and sizes - /// First binding point /// Number of bindings /// Indicates if the buffers are storage or uniform buffers [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -866,7 +865,6 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Buffer texture /// Physical ranges of memory where the buffer texture data is located /// Binding info for the buffer texture - /// Format of the buffer texture /// Whether the binding is for an image or a sampler public void SetBufferTextureStorage( ShaderStage stage, @@ -889,7 +887,6 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Physical ranges of memory where the buffer texture data is located /// Binding info for the buffer texture /// Index of the binding on the array - /// Format of the buffer texture public void SetBufferTextureStorage( ShaderStage stage, ITextureArray array, @@ -912,7 +909,6 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Physical ranges of memory where the buffer texture data is located /// Binding info for the buffer texture /// Index of the binding on the array - /// Format of the buffer texture public void SetBufferTextureStorage( ShaderStage stage, IImageArray array, diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs index bccbdfd31..8c1132d9b 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferModifiedRangeList.cs @@ -1,25 +1,24 @@ -using Ryujinx.Common.Pools; using Ryujinx.Memory.Range; using System; +using System.Collections.Generic; using System.Linq; -using System.Threading; namespace Ryujinx.Graphics.Gpu.Memory { /// /// A range within a buffer that has been modified by the GPU. /// - class BufferModifiedRange : IRange + class BufferModifiedRange : INonOverlappingRange { /// /// Start address of the range in guest memory. /// - public ulong Address { get; } + public ulong Address { get; internal set; } /// /// Size of the range in bytes. /// - public ulong Size { get; } + public ulong Size { get; internal set; } /// /// End address of the range in guest memory. @@ -61,14 +60,19 @@ namespace Ryujinx.Graphics.Gpu.Memory { return Address < address + size && address < EndAddress; } + + public INonOverlappingRange Split(ulong splitAddress) + { + throw new NotImplementedException(); + } } /// /// A structure used to track GPU modified ranges within a buffer. /// - class BufferModifiedRangeList : RangeList + class BufferModifiedRangeList : NonOverlappingRangeList { - private const int BackingInitialSize = 8; + private new const int BackingInitialSize = 8; private readonly GpuContext _context; private readonly Buffer _parent; @@ -77,8 +81,6 @@ namespace Ryujinx.Graphics.Gpu.Memory private BufferMigration _source; private BufferModifiedRangeList _migrationTarget; - private readonly Lock _lock = new(); - /// /// Whether the modified range list has any entries or not. /// @@ -86,10 +88,10 @@ namespace Ryujinx.Graphics.Gpu.Memory { get { - lock (_lock) - { - return Count > 0; - } + Lock.EnterReadLock(); + bool result = Count > 0; + Lock.ExitReadLock(); + return result; } } @@ -114,33 +116,41 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Action to perform for each remaining sub-range of the input range public void ExcludeModifiedRegions(ulong address, ulong size, Action action) { - lock (_lock) + // Slices a given region using the modified regions in the list. Calls the action for the new slices. + bool lockOwner = Lock.IsReadLockHeld; + if (!lockOwner) { - // Slices a given region using the modified regions in the list. Calls the action for the new slices. - ref BufferModifiedRange[] overlaps = ref ThreadStaticArray.Get(); + Lock.EnterReadLock(); + } - int count = FindOverlapsNonOverlapping(address, size, ref overlaps); + (RangeItem first, RangeItem last) = FindOverlaps(address, size); - for (int i = 0; i < count; i++) + RangeItem current = first; + while (last != null && current != last.Next) + { + BufferModifiedRange overlap = current.Value; + + if (overlap.Address > address) { - BufferModifiedRange overlap = overlaps[i]; - - if (overlap.Address > address) - { - // The start of the remaining region is uncovered by this overlap. Call the action for it. - action(address, overlap.Address - address); - } - - // Remaining region is after this overlap. - size -= overlap.EndAddress - address; - address = overlap.EndAddress; + // The start of the remaining region is uncovered by this overlap. Call the action for it. + action(address, overlap.Address - address); } - if ((long)size > 0) - { - // If there is any region left after removing the overlaps, signal it. - action(address, size); - } + // Remaining region is after this overlap. + size -= overlap.EndAddress - address; + address = overlap.EndAddress; + current = current.Next; + } + + if (!lockOwner) + { + Lock.ExitReadLock(); + } + + if ((long)size > 0) + { + // If there is any region left after removing the overlaps, signal it. + action(address, size); } } @@ -152,51 +162,101 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Size of the modified region in bytes public void SignalModified(ulong address, ulong size) { - // Must lock, as this can affect flushes from the background thread. - lock (_lock) + // We may overlap with some existing modified regions. They must be cut into by the new entry. + Lock.EnterWriteLock(); + (RangeItem first, RangeItem last) = FindOverlaps(address, size); + + ulong endAddress = address + size; + ulong syncNumber = _context.SyncNumber; + + if (first is null) { - // We may overlap with some existing modified regions. They must be cut into by the new entry. - ref BufferModifiedRange[] overlaps = ref ThreadStaticArray.Get(); + Add(new BufferModifiedRange(address, size, syncNumber, this)); + Lock.ExitWriteLock(); + return; + } - int count = FindOverlapsNonOverlapping(address, size, ref overlaps); + BufferModifiedRange buffPost = null; + bool extendsPost = false; + bool extendsPre = false; - ulong endAddress = address + size; - ulong syncNumber = _context.SyncNumber; - - for (int i = 0; i < count; i++) + if (first == last) + { + if (first.Address == address && first.EndAddress == endAddress) { - // The overlaps must be removed or split. + first.Value.SyncNumber = syncNumber; + first.Value.Parent = this; + Lock.ExitWriteLock(); + return; + } - BufferModifiedRange overlap = overlaps[i]; + if (first.Address < address) + { + first.Value.Size = address - first.Address; - if (overlap.Address == address && overlap.Size == size) + extendsPre = true; + + if (first.EndAddress > endAddress) { - // Region already exists. Just update the existing sync number. - overlap.SyncNumber = syncNumber; - overlap.Parent = this; - - return; + buffPost = new BufferModifiedRange(endAddress, first.EndAddress - endAddress, + first.Value.SyncNumber, first.Value.Parent); + extendsPost = true; } - - Remove(overlap); - - if (overlap.Address < address && overlap.EndAddress > address) + } + else + { + if (first.EndAddress > endAddress) { - // A split item must be created behind this overlap. - - Add(new BufferModifiedRange(overlap.Address, address - overlap.Address, overlap.SyncNumber, overlap.Parent)); + first.Value.Size = first.EndAddress - endAddress; + first.Value.Address = endAddress; } - - if (overlap.Address < endAddress && overlap.EndAddress > endAddress) + else { - // A split item must be created after this overlap. - - Add(new BufferModifiedRange(endAddress, overlap.EndAddress - endAddress, overlap.SyncNumber, overlap.Parent)); + Remove(first.Value); } } + if (extendsPre && extendsPost) + { + Add(buffPost); + } + Add(new BufferModifiedRange(address, size, syncNumber, this)); + Lock.ExitWriteLock(); + + return; } + + BufferModifiedRange buffPre = null; + + if (first.Address < address) + { + buffPre = new BufferModifiedRange(first.Address, address - first.Address, + first.Value.SyncNumber, first.Value.Parent); + extendsPre = true; + } + + if (last.EndAddress > endAddress) + { + buffPost = new BufferModifiedRange(endAddress, last.EndAddress - endAddress, + last.Value.SyncNumber, last.Value.Parent); + extendsPost = true; + } + + RemoveRange(first, last); + + if (extendsPre) + { + Add(buffPre); + } + + if (extendsPost) + { + Add(buffPost); + } + + Add(new BufferModifiedRange(address, size, syncNumber, this)); + Lock.ExitWriteLock(); } /// @@ -208,25 +268,23 @@ namespace Ryujinx.Graphics.Gpu.Memory /// The action to call for each modified range public void GetRangesAtSync(ulong address, ulong size, ulong syncNumber, Action rangeAction) { - int count = 0; + Lock.EnterReadLock(); + (RangeItem first, RangeItem last) = FindOverlaps(address, size); - ref BufferModifiedRange[] overlaps = ref ThreadStaticArray.Get(); - - // Range list must be consistent for this operation. - lock (_lock) + RangeItem current = first; + while (last != null && current != last.Next) { - count = FindOverlapsNonOverlapping(address, size, ref overlaps); - } - - for (int i = 0; i < count; i++) - { - BufferModifiedRange overlap = overlaps[i]; + BufferModifiedRange overlap = current.Value; if (overlap.SyncNumber == syncNumber) { rangeAction(overlap.Address, overlap.Size); } + + current = current.Next; } + + Lock.ExitReadLock(); } /// @@ -237,19 +295,23 @@ namespace Ryujinx.Graphics.Gpu.Memory /// The action to call for each modified range public void GetRanges(ulong address, ulong size, Action rangeAction) { - int count = 0; - - ref BufferModifiedRange[] overlaps = ref ThreadStaticArray.Get(); - - // Range list must be consistent for this operation. - lock (_lock) + List> overlaps = []; + + // We use the non-span method here because keeping the lock will cause a deadlock. + Lock.EnterReadLock(); + (RangeItem first, RangeItem last) = FindOverlaps(address, size); + + RangeItem current = first; + while (last != null && current != last.Next) { - count = FindOverlapsNonOverlapping(address, size, ref overlaps); + overlaps.Add(current); + current = current.Next; } + Lock.ExitReadLock(); - for (int i = 0; i < count; i++) + for (int i = 0; i < overlaps.Count; i++) { - BufferModifiedRange overlap = overlaps[i]; + BufferModifiedRange overlap = overlaps[i].Value; rangeAction(overlap.Address, overlap.Size); } } @@ -262,11 +324,11 @@ namespace Ryujinx.Graphics.Gpu.Memory /// True if a range exists in the specified region, false otherwise public bool HasRange(ulong address, ulong size) { - // Range list must be consistent for this operation. - lock (_lock) - { - return FindOverlapsNonOverlapping(address, size, ref ThreadStaticArray.Get()) > 0; - } + Lock.EnterReadLock(); + (RangeItem first, RangeItem _) = FindOverlaps(address, size); + bool result = first is not null; + Lock.ExitReadLock(); + return result; } /// @@ -298,38 +360,37 @@ namespace Ryujinx.Graphics.Gpu.Memory /// The start address of the flush range /// The end address of the flush range private void RemoveRangesAndFlush( - BufferModifiedRange[] overlaps, + RangeItem[] overlaps, int rangeCount, long highestDiff, ulong currentSync, ulong address, ulong endAddress) { - lock (_lock) + if (_migrationTarget == null) { - if (_migrationTarget == null) + ulong waitSync = currentSync + (ulong)highestDiff; + + for (int i = 0; i < rangeCount; i++) { - ulong waitSync = currentSync + (ulong)highestDiff; + BufferModifiedRange overlap = overlaps[i].Value; - for (int i = 0; i < rangeCount; i++) + long diff = (long)(overlap.SyncNumber - currentSync); + + if (diff <= highestDiff) { - BufferModifiedRange overlap = overlaps[i]; + ulong clampAddress = Math.Max(address, overlap.Address); + ulong clampEnd = Math.Min(endAddress, overlap.EndAddress); - long diff = (long)(overlap.SyncNumber - currentSync); + Lock.EnterWriteLock(); + ClearPart(overlap, clampAddress, clampEnd); + Lock.ExitWriteLock(); - if (diff <= highestDiff) - { - ulong clampAddress = Math.Max(address, overlap.Address); - ulong clampEnd = Math.Min(endAddress, overlap.EndAddress); - - ClearPart(overlap, clampAddress, clampEnd); - - RangeActionWithMigration(clampAddress, clampEnd - clampAddress, waitSync, _flushAction); - } + RangeActionWithMigration(clampAddress, clampEnd - clampAddress, waitSync, _flushAction); } - - return; } + + return; } // There is a migration target to call instead. This can't be changed after set so accessing it outside the lock is fine. @@ -355,28 +416,37 @@ namespace Ryujinx.Graphics.Gpu.Memory int rangeCount = 0; - ref BufferModifiedRange[] overlaps = ref ThreadStaticArray.Get(); + List> overlaps = []; // Range list must be consistent for this operation - lock (_lock) + Lock.EnterReadLock(); + if (_migrationTarget != null) { - if (_migrationTarget != null) + rangeCount = -1; + } + else + { + // We use the non-span method here because the array is partially modified by the code, which would invalidate a span. + (RangeItem first, RangeItem last) = FindOverlaps(address, size); + + RangeItem current = first; + while (last != null && current != last.Next) { - rangeCount = -1; - } - else - { - rangeCount = FindOverlapsNonOverlapping(address, size, ref overlaps); + rangeCount++; + overlaps.Add(current); + current = current.Next; } } + Lock.ExitReadLock(); if (rangeCount == -1) { - _migrationTarget.WaitForAndFlushRanges(address, size); + _migrationTarget!.WaitForAndFlushRanges(address, size); return; } - else if (rangeCount == 0) + + if (rangeCount == 0) { return; } @@ -388,7 +458,7 @@ namespace Ryujinx.Graphics.Gpu.Memory for (int i = 0; i < rangeCount; i++) { - BufferModifiedRange overlap = overlaps[i]; + BufferModifiedRange overlap = overlaps[i].Value; long diff = (long)(overlap.SyncNumber - currentSync); @@ -406,7 +476,7 @@ namespace Ryujinx.Graphics.Gpu.Memory // Wait for the syncpoint. _context.Renderer.WaitSync(currentSync + (ulong)highestDiff); - RemoveRangesAndFlush(overlaps, rangeCount, highestDiff, currentSync, address, endAddress); + RemoveRangesAndFlush(overlaps.ToArray(), rangeCount, highestDiff, currentSync, address, endAddress); } /// @@ -419,42 +489,39 @@ namespace Ryujinx.Graphics.Gpu.Memory /// The action to call for each modified range public void InheritRanges(BufferModifiedRangeList ranges, Action registerRangeAction) { - BufferModifiedRange[] inheritRanges; + ranges.Lock.EnterReadLock(); + BufferModifiedRange[] inheritRanges = ranges.ToArray(); + ranges.Lock.ExitReadLock(); - lock (ranges._lock) + // Copy over the migration from the previous range list + + BufferMigration oldMigration = ranges._source; + + BufferMigrationSpan span = new(ranges._parent, ranges._flushAction, oldMigration); + ranges._parent.IncrementReferenceCount(); + + if (_source == null) { - inheritRanges = ranges.ToArray(); + // Create a new migration. + _source = new BufferMigration([span], this, _context.SyncNumber); - lock (_lock) - { - // Copy over the migration from the previous range list - - BufferMigration oldMigration = ranges._source; - - BufferMigrationSpan span = new(ranges._parent, ranges._flushAction, oldMigration); - ranges._parent.IncrementReferenceCount(); - - if (_source == null) - { - // Create a new migration. - _source = new BufferMigration([span], this, _context.SyncNumber); - - _context.RegisterBufferMigration(_source); - } - else - { - // Extend the migration - _source.AddSpanToEnd(span); - } - - ranges._migrationTarget = this; - - foreach (BufferModifiedRange range in inheritRanges) - { - Add(range); - } - } + _context.RegisterBufferMigration(_source); } + else + { + // Extend the migration + _source.AddSpanToEnd(span); + } + + ranges._migrationTarget = this; + + Lock.EnterWriteLock(); + foreach (BufferModifiedRange range in inheritRanges) + { + Add(range); + } + + Lock.ExitWriteLock(); ulong currentSync = _context.SyncNumber; foreach (BufferModifiedRange range in inheritRanges) @@ -473,18 +540,18 @@ namespace Ryujinx.Graphics.Gpu.Memory /// public void SelfMigration() { - lock (_lock) - { - BufferMigrationSpan span = new(_parent, _parent.GetSnapshotDisposeAction(), _parent.GetSnapshotFlushAction(), _source); - BufferMigration migration = new([span], this, _context.SyncNumber); + BufferMigrationSpan span = new(_parent, _parent.GetSnapshotDisposeAction(), + _parent.GetSnapshotFlushAction(), _source); + BufferMigration migration = new([span], this, _context.SyncNumber); - // Migration target is used to redirect flush actions to the latest range list, - // so we don't need to set it here. (this range list is still the latest) + // Migration target is used to redirect flush actions to the latest range list, + // so we don't need to set it here. (this range list is still the latest) - _context.RegisterBufferMigration(migration); + _context.RegisterBufferMigration(migration); - _source = migration; - } + Lock.EnterWriteLock(); + _source = migration; + Lock.ExitWriteLock(); } /// @@ -493,13 +560,13 @@ namespace Ryujinx.Graphics.Gpu.Memory /// The migration to remove public void RemoveMigration(BufferMigration migration) { - lock (_lock) + Lock.EnterWriteLock(); + if (_source == migration) { - if (_source == migration) - { - _source = null; - } + _source = null; } + + Lock.ExitWriteLock(); } private void ClearPart(BufferModifiedRange overlap, ulong address, ulong endAddress) @@ -526,33 +593,85 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Size to clear public void Clear(ulong address, ulong size) { - lock (_lock) + ulong endAddress = address + size; + Lock.EnterWriteLock(); + (RangeItem first, RangeItem last) = FindOverlaps(address, size); + + if (first is null) { - // This function can be called from any thread, so it cannot use the arrays for background or foreground. - BufferModifiedRange[] toClear = new BufferModifiedRange[1]; + Lock.ExitWriteLock(); + return; + } - int rangeCount = FindOverlapsNonOverlapping(address, size, ref toClear); + BufferModifiedRange buffPost = null; + bool extendsPost = false; + bool extendsPre = false; - ulong endAddress = address + size; - - for (int i = 0; i < rangeCount; i++) + if (first == last) + { + if (first.Address < address) { - BufferModifiedRange overlap = toClear[i]; + first.Value.Size = address - first.Address; + extendsPre = true; - ClearPart(overlap, address, endAddress); + if (first.EndAddress > endAddress) + { + buffPost = new BufferModifiedRange(endAddress, first.EndAddress - endAddress, + first.Value.SyncNumber, first.Value.Parent); + extendsPost = true; + } + } + else + { + if (first.EndAddress > endAddress) + { + first.Value.Size = first.EndAddress - endAddress; + first.Value.Address = endAddress; + } + else + { + Remove(first.Value); + } } - } - } - /// - /// Clear all modified ranges. - /// - public void Clear() - { - lock (_lock) - { - Count = 0; + if (extendsPre && extendsPost) + { + Add(buffPost); + } + + Lock.ExitWriteLock(); + return; } + + BufferModifiedRange buffPre = null; + + if (first.Address < address) + { + buffPre = new BufferModifiedRange(first.Address, address - first.Address, + first.Value.SyncNumber, first.Value.Parent); + extendsPre = true; + } + + if (last.EndAddress > endAddress) + { + buffPost = new BufferModifiedRange(endAddress, last.EndAddress - endAddress, + last.Value.SyncNumber, last.Value.Parent); + extendsPost = true; + } + + RemoveRange(first, last); + + if (extendsPre) + { + Add(buffPre); + } + + if (extendsPost) + { + Add(buffPost); + } + + Lock.ExitWriteLock(); } } } diff --git a/src/Ryujinx.Graphics.Gpu/Memory/BufferStage.cs b/src/Ryujinx.Graphics.Gpu/Memory/BufferStage.cs index 8a9f37658..c299731f8 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/BufferStage.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/BufferStage.cs @@ -1,4 +1,5 @@ using Ryujinx.Graphics.Shader; +using System; using System.Runtime.CompilerServices; namespace Ryujinx.Graphics.Gpu.Memory @@ -7,6 +8,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// Pipeline stages that can modify buffer data, as well as flags indicating storage usage. /// Must match ShaderStage for the shader stages, though anything after that can be in any order. /// + [Flags] internal enum BufferStage : byte { Compute, diff --git a/src/Ryujinx.Graphics.Gpu/Memory/MemoryManager.cs b/src/Ryujinx.Graphics.Gpu/Memory/MemoryManager.cs index 6efb7f334..95e43e341 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/MemoryManager.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/MemoryManager.cs @@ -690,11 +690,8 @@ namespace Ryujinx.Graphics.Gpu.Memory if (_pageTable[l0] == null) { _pageTable[l0] = new ulong[PtLvl1Size]; - - for (ulong index = 0; index < PtLvl1Size; index++) - { - _pageTable[l0][index] = PteUnmapped; - } + + Array.Fill(_pageTable[l0], PteUnmapped); } _pageTable[l0][l1] = pte; diff --git a/src/Ryujinx.Graphics.Gpu/Memory/VirtualRangeCache.cs b/src/Ryujinx.Graphics.Gpu/Memory/VirtualRangeCache.cs index ac25b3e5d..06253cb30 100644 --- a/src/Ryujinx.Graphics.Gpu/Memory/VirtualRangeCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Memory/VirtualRangeCache.cs @@ -15,7 +15,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Represents a GPU virtual memory range. /// - private readonly struct VirtualRange : IRange + private class VirtualRange : INonOverlappingRange { /// /// GPU virtual address where the range starts. @@ -25,7 +25,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Size of the range in bytes. /// - public ulong Size { get; } + public ulong Size { get; private set; } /// /// GPU virtual address where the range ends. @@ -35,7 +35,7 @@ namespace Ryujinx.Graphics.Gpu.Memory /// /// Physical regions where the GPU virtual region is mapped. /// - public MultiRange Range { get; } + public MultiRange Range { get; private set; } /// /// Creates a new virtual memory range. @@ -60,10 +60,14 @@ namespace Ryujinx.Graphics.Gpu.Memory { return Address < address + size && address < EndAddress; } + + public INonOverlappingRange Split(ulong splitAddress) + { + throw new NotImplementedException(); + } } - private readonly RangeList _virtualRanges; - private VirtualRange[] _virtualRangeOverlaps; + private readonly NonOverlappingRangeList _virtualRanges; private readonly ConcurrentQueue _deferredUnmaps; private int _hasDeferredUnmaps; @@ -75,7 +79,6 @@ namespace Ryujinx.Graphics.Gpu.Memory { _memoryManager = memoryManager; _virtualRanges = []; - _virtualRangeOverlaps = new VirtualRange[BufferCache.OverlapsBufferInitialCapacity]; _deferredUnmaps = new ConcurrentQueue(); } @@ -106,19 +109,11 @@ namespace Ryujinx.Graphics.Gpu.Memory /// True if the range already existed, false if a new one was created and added public bool TryGetOrAddRange(ulong gpuVa, ulong size, out MultiRange range) { - VirtualRange[] overlaps = _virtualRangeOverlaps; - int overlapsCount; - if (Interlocked.Exchange(ref _hasDeferredUnmaps, 0) != 0) { while (_deferredUnmaps.TryDequeue(out VirtualRange unmappedRange)) { - overlapsCount = _virtualRanges.FindOverlapsNonOverlapping(unmappedRange.Address, unmappedRange.Size, ref overlaps); - - for (int index = 0; index < overlapsCount; index++) - { - _virtualRanges.Remove(overlaps[index]); - } + _virtualRanges.RemoveRange(unmappedRange.Address, unmappedRange.Size); } } @@ -126,27 +121,22 @@ namespace Ryujinx.Graphics.Gpu.Memory ulong originalVa = gpuVa; - overlapsCount = _virtualRanges.FindOverlapsNonOverlapping(gpuVa, size, ref overlaps); - - if (overlapsCount != 0) + _virtualRanges.Lock.EnterWriteLock(); + (RangeItem first, RangeItem last) = _virtualRanges.FindOverlaps(gpuVa, size); + + if (first is not null) { // The virtual range already exists. We just need to check if our range fits inside // the existing one, and if not, we must extend the existing one. ulong endAddress = gpuVa + size; - VirtualRange overlap0 = overlaps[0]; - if (overlap0.Address > gpuVa || overlap0.EndAddress < endAddress) + if (first.Address > gpuVa || first.EndAddress < endAddress) { - for (int index = 0; index < overlapsCount; index++) - { - VirtualRange virtualRange = overlaps[index]; - - gpuVa = Math.Min(gpuVa, virtualRange.Address); - endAddress = Math.Max(endAddress, virtualRange.EndAddress); - - _virtualRanges.Remove(virtualRange); - } + gpuVa = Math.Min(gpuVa, first.Address); + endAddress = Math.Max(endAddress, last.EndAddress); + + _virtualRanges.RemoveRange(first, last); ulong newSize = endAddress - gpuVa; MultiRange newRange = _memoryManager.GetPhysicalRegions(gpuVa, newSize); @@ -157,8 +147,8 @@ namespace Ryujinx.Graphics.Gpu.Memory } else { - found = overlap0.Range.Count == 1 || IsSparseAligned(overlap0.Range); - range = overlap0.Range.Slice(gpuVa - overlap0.Address, size); + found = first.Value.Range.Count == 1 || IsSparseAligned(first.Value.Range); + range = first.Value.Range.Slice(gpuVa - first.Address, size); } } else @@ -170,8 +160,7 @@ namespace Ryujinx.Graphics.Gpu.Memory _virtualRanges.Add(virtualRange); } - - ShrinkOverlapsBufferIfNeeded(); + _virtualRanges.Lock.ExitWriteLock(); // If the range is not properly aligned for sparse mapping, // let's just force it to a single range. @@ -221,16 +210,5 @@ namespace Ryujinx.Graphics.Gpu.Memory return true; } - - /// - /// Resizes the temporary buffer used for range list intersection results, if it has grown too much. - /// - private void ShrinkOverlapsBufferIfNeeded() - { - if (_virtualRangeOverlaps.Length > BufferCache.OverlapsBufferMaxCapacity) - { - Array.Resize(ref _virtualRangeOverlaps, BufferCache.OverlapsBufferMaxCapacity); - } - } } } diff --git a/src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryBlockManager.cs b/src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryBlockManager.cs index f13a3554a..6a57aa3d1 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryBlockManager.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Memory/KMemoryBlockManager.cs @@ -89,13 +89,19 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory if (baseAddress > currBaseAddr) { KMemoryBlock newBlock = currBlock.SplitRightAtAddress(baseAddress); - _blockTree.Add(newBlock); + if (currBlock.Left == null) + _blockTree.Add(newBlock, currBlock); + else + _blockTree.Add(newBlock, currBlock.Predecessor); } if (endAddr < currEndAddr) { KMemoryBlock newBlock = currBlock.SplitRightAtAddress(endAddr); - _blockTree.Add(newBlock); + if (currBlock.Left == null) + _blockTree.Add(newBlock, currBlock); + else + _blockTree.Add(newBlock, currBlock.Predecessor); currBlock = newBlock; } @@ -143,13 +149,19 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory if (baseAddress > currBaseAddr) { KMemoryBlock newBlock = currBlock.SplitRightAtAddress(baseAddress); - _blockTree.Add(newBlock); + if (currBlock.Left == null) + _blockTree.Add(newBlock, currBlock); + else + _blockTree.Add(newBlock, currBlock.Predecessor); } if (endAddr < currEndAddr) { KMemoryBlock newBlock = currBlock.SplitRightAtAddress(endAddr); - _blockTree.Add(newBlock); + if (currBlock.Left == null) + _blockTree.Add(newBlock, currBlock); + else + _blockTree.Add(newBlock, currBlock.Predecessor); currBlock = newBlock; } @@ -199,13 +211,19 @@ namespace Ryujinx.HLE.HOS.Kernel.Memory if (baseAddress > currBaseAddr) { KMemoryBlock newBlock = currBlock.SplitRightAtAddress(baseAddress); - _blockTree.Add(newBlock); + if (currBlock.Left == null) + _blockTree.Add(newBlock, currBlock); + else + _blockTree.Add(newBlock, currBlock.Predecessor); } if (endAddr < currEndAddr) { KMemoryBlock newBlock = currBlock.SplitRightAtAddress(endAddr); - _blockTree.Add(newBlock); + if (currBlock.Left == null) + _blockTree.Add(newBlock, currBlock); + else + _blockTree.Add(newBlock, currBlock.Predecessor); currBlock = newBlock; } diff --git a/src/Ryujinx.Memory/Range/INonOverlappingRange.cs b/src/Ryujinx.Memory/Range/INonOverlappingRange.cs index 23194e0f2..c6a0197d4 100644 --- a/src/Ryujinx.Memory/Range/INonOverlappingRange.cs +++ b/src/Ryujinx.Memory/Range/INonOverlappingRange.cs @@ -3,7 +3,7 @@ namespace Ryujinx.Memory.Range /// /// Range of memory that can be split in two. /// - interface INonOverlappingRange : IRange + public interface INonOverlappingRange : IRange { /// /// Split this region into two, around the specified address. diff --git a/src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs b/src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs index 894078aee..7803b03d1 100644 --- a/src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs +++ b/src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs @@ -1,5 +1,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; namespace Ryujinx.Memory.Range { @@ -7,8 +10,284 @@ namespace Ryujinx.Memory.Range /// A range list that assumes ranges are non-overlapping, with list items that can be split in two to avoid overlaps. /// /// Type of the range. - class NonOverlappingRangeList : RangeList where T : INonOverlappingRange + public class NonOverlappingRangeList : RangeListBase where T : INonOverlappingRange { + private readonly Dictionary> _quickAccess = new(AddressEqualityComparer.Comparer); + private readonly Dictionary> _fastQuickAccess = new(AddressEqualityComparer.Comparer); + + public readonly ReaderWriterLockSlim Lock = new(); + + /// + /// Creates a new non-overlapping range list. + /// + public NonOverlappingRangeList() { } + + /// + /// Creates a new non-overlapping range list. + /// + /// The initial size of the backing array + public NonOverlappingRangeList(int backingInitialSize) : base(backingInitialSize) { } + + /// + /// Adds a new item to the list. + /// + /// The item to be added + public override void Add(T item) + { + int index = BinarySearch(item.Address); + + if (index < 0) + { + index = ~index; + } + + RangeItem rangeItem = new(item); + + Insert(index, rangeItem); + + _quickAccess.Add(item.Address, rangeItem); + } + + /// + /// Updates an item's end address on the list. Address must be the same. + /// + /// The item to be updated + /// True if the item was located and updated, false otherwise + protected override bool Update(T item) + { + int index = BinarySearch(item.Address); + + if (index >= 0 && Items[index].Value.Equals(item)) + { + RangeItem rangeItem = new(item) { Previous = Items[index].Previous, Next = Items[index].Next }; + + if (index > 0) + { + Items[index - 1].Next = rangeItem; + } + + if (index < Count - 1) + { + Items[index + 1].Previous = rangeItem; + } + + foreach (ulong addr in Items[index].QuickAccessAddresses) + { + _quickAccess.Remove(addr); + _fastQuickAccess.Remove(addr); + } + + Items[index] = rangeItem; + + _quickAccess[item.Address] = rangeItem; + + return true; + } + + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void Insert(int index, RangeItem item) + { + Debug.Assert(item.Address != item.EndAddress); + + if (Count + 1 > Items.Length) + { + Array.Resize(ref Items, Items.Length + BackingGrowthSize); + } + + if (index >= Count) + { + if (index == Count) + { + if (index != 0) + { + item.Previous = Items[index - 1]; + Items[index - 1].Next = item; + } + Items[index] = item; + Count++; + } + } + else + { + Array.Copy(Items, index, Items, index + 1, Count - index); + + Items[index] = item; + if (index != 0) + { + item.Previous = Items[index - 1]; + Items[index - 1].Next = item; + } + + item.Next = Items[index + 1]; + Items[index + 1].Previous = item; + + Count++; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void RemoveAt(int index) + { + if (index < Count - 1) + { + Items[index + 1].Previous = index > 0 ? Items[index - 1] : null; + } + + if (index > 0) + { + Items[index - 1].Next = index < Count - 1 ? Items[index + 1] : null; + } + + if (index < --Count) + { + Array.Copy(Items, index + 1, Items, index, Count - index); + } + } + + /// + /// Removes an item from the list. + /// + /// The item to be removed + /// True if the item was removed, or false if it was not found + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override bool Remove(T item) + { + int index = BinarySearch(item.Address); + + if (index >= 0 && Items[index].Value.Equals(item)) + { + _quickAccess.Remove(item.Address); + + foreach (ulong addr in Items[index].QuickAccessAddresses) + { + _quickAccess.Remove(addr); + _fastQuickAccess.Remove(addr); + } + + RemoveAt(index); + + return true; + } + + return false; + } + + /// + /// Removes a range of items from the item list + /// + /// The first item in the range of items to be removed + /// The last item in the range of items to be removed + public override void RemoveRange(RangeItem startItem, RangeItem endItem) + { + if (startItem is null) + { + return; + } + + if (startItem == endItem) + { + Remove(startItem.Value); + return; + } + + int startIndex = BinarySearch(startItem.Address); + int endIndex = BinarySearch(endItem.Address); + + if (endIndex < Count - 1) + { + Items[endIndex + 1].Previous = startIndex > 0 ? Items[startIndex - 1] : null; + } + + if (startIndex > 0) + { + Items[startIndex - 1].Next = endIndex < Count - 1 ? Items[endIndex + 1] : null; + } + + + if (endIndex < Count - 1) + { + Array.Copy(Items, endIndex + 1, Items, startIndex, Count - endIndex - 1); + } + + Count -= endIndex - startIndex + 1; + + while (startItem != endItem.Next) + { + _quickAccess.Remove(startItem.Address); + foreach (ulong addr in startItem.QuickAccessAddresses) + { + _quickAccess.Remove(addr); + _fastQuickAccess.Remove(addr); + } + startItem = startItem.Next; + } + } + + /// + /// Removes a range of items from the item list + /// + /// Start address of the range + /// Size of the range + public void RemoveRange(ulong address, ulong size) + { + int startIndex = BinarySearchLeftEdge(address, address + size); + + if (startIndex < 0) + { + return; + } + + RangeItem startItem = Items[startIndex]; + + int endIndex = startIndex; + + while (startItem is not null && startItem.Address < address + size) + { + _quickAccess.Remove(startItem.Address); + foreach (ulong addr in startItem.QuickAccessAddresses) + { + _quickAccess.Remove(addr); + _fastQuickAccess.Remove(addr); + } + startItem = startItem.Next; + endIndex++; + } + + if (endIndex < Count - 1) + { + Items[endIndex + 1].Previous = startIndex > 0 ? Items[startIndex - 1] : null; + } + + if (startIndex > 0) + { + Items[startIndex - 1].Next = endIndex < Count - 1 ? Items[endIndex + 1] : null; + } + + + if (endIndex < Count - 1) + { + Array.Copy(Items, endIndex + 1, Items, startIndex, Count - endIndex - 1); + } + + Count -= endIndex - startIndex + 1; + } + + /// + /// Clear all ranges. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Clear() + { + Lock.EnterWriteLock(); + Count = 0; + _quickAccess.Clear(); + _fastQuickAccess.Clear(); + Lock.ExitWriteLock(); + } + /// /// Finds a list of regions that cover the desired (address, size) range. /// If this range starts or ends in the middle of an existing region, it is split and only the relevant part is added. @@ -19,17 +298,18 @@ namespace Ryujinx.Memory.Range /// Start address of the search region /// Size of the search region /// Factory for creating new ranges - public void GetOrAddRegions(List list, ulong address, ulong size, Func factory) + public void GetOrAddRegions(out List list, ulong address, ulong size, Func factory) { // (regarding the specific case this generalized function is used for) // A new region may be split into multiple parts if multiple virtual regions have mapped to it. // For instance, while a virtual mapping could cover 0-2 in physical space, the space 0-1 may have already been reserved... // So we need to return both the split 0-1 and 1-2 ranges. - - T[] results = new T[1]; - int count = FindOverlapsNonOverlapping(address, size, ref results); - - if (count == 0) + + Lock.EnterWriteLock(); + (RangeItem first, RangeItem last) = FindOverlaps(address, size); + list = new List(); + + if (first is null) { // The region is fully unmapped. Create and add it to the range list. T region = factory(address, size); @@ -41,13 +321,15 @@ namespace Ryujinx.Memory.Range ulong lastAddress = address; ulong endAddress = address + size; - for (int i = 0; i < count; i++) + RangeItem current = first; + while (last is not null && current is not null && current.Address < endAddress) { - T region = results[i]; - if (count == 1 && region.Address == address && region.Size == size) + T region = current.Value; + if (first == last && region.Address == address && region.Size == size) { // Exact match, no splitting required. list.Add(region); + Lock.ExitWriteLock(); return; } @@ -75,6 +357,7 @@ namespace Ryujinx.Memory.Range list.Add(region); lastAddress = region.EndAddress; + current = current.Next; } if (lastAddress < endAddress) @@ -85,6 +368,8 @@ namespace Ryujinx.Memory.Range Add(fillRegion); } } + + Lock.ExitWriteLock(); } /// @@ -95,6 +380,7 @@ namespace Ryujinx.Memory.Range /// The region to split /// The address to split with /// The new region (high part) + [MethodImpl(MethodImplOptions.AggressiveInlining)] private T Split(T region, ulong splitAddress) { T newRegion = (T)region.Split(splitAddress); @@ -102,5 +388,113 @@ namespace Ryujinx.Memory.Range Add(newRegion); return newRegion; } + + /// + /// Gets an item on the list overlapping the specified memory range. + /// + /// Start address of the range + /// Size in bytes of the range + /// The leftmost overlapping item, or null if none is found + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override RangeItem FindOverlap(ulong address, ulong size) + { + if (_quickAccess.TryGetValue(address, out RangeItem overlap)) + { + return overlap; + } + + int index = BinarySearchLeftEdge(address, address + size); + + if (index < 0) + { + return null; + } + + if (Items[index].Address < address) + { + _quickAccess.TryAdd(address, Items[index]); + Items[index].QuickAccessAddresses.Add(address); + } + + return Items[index]; + } + + /// + /// Gets an item on the list overlapping the specified memory range. + /// + /// Start address of the range + /// Size in bytes of the range + /// The overlapping item, or null if none is found + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override RangeItem FindOverlapFast(ulong address, ulong size) + { + if (_quickAccess.TryGetValue(address, out RangeItem overlap) || _fastQuickAccess.TryGetValue(address, out overlap)) + { + return overlap; + } + + int index = BinarySearch(address, address + size); + + if (index < 0) + { + return null; + } + + if (Items[index].Address < address) + { + _quickAccess.TryAdd(address, Items[index]); + } + else + { + _fastQuickAccess.TryAdd(address, Items[index]); + } + + Items[index].QuickAccessAddresses.Add(address); + + return Items[index]; + } + + /// + /// Gets all items on the list overlapping the specified memory range. + /// + /// Start address of the range + /// Size in bytes of the range + /// The first and last overlapping items, or null if none are found + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public (RangeItem, RangeItem) FindOverlaps(ulong address, ulong size) + { + if (_quickAccess.TryGetValue(address, out RangeItem overlap)) + { + if (overlap.Next is null || overlap.Next.Address >= address + size) + { + return (overlap, overlap); + } + + return (overlap, Items[BinarySearchRightEdge(address, address + size)]); + } + + (int index, int endIndex) = BinarySearchEdges(address, address + size); + + if (index < 0) + { + return (null, null); + } + + if (Items[index].Address < address) + { + _quickAccess.TryAdd(address, Items[index]); + Items[index].QuickAccessAddresses.Add(address); + } + + return (Items[index], Items[endIndex - 1]); + } + + public override IEnumerator GetEnumerator() + { + for (int i = 0; i < Count; i++) + { + yield return Items[i].Value; + } + } } } diff --git a/src/Ryujinx.Memory/Range/RangeList.cs b/src/Ryujinx.Memory/Range/RangeList.cs index 72cef1de0..600ff748e 100644 --- a/src/Ryujinx.Memory/Range/RangeList.cs +++ b/src/Ryujinx.Memory/Range/RangeList.cs @@ -1,61 +1,91 @@ using System; -using System.Collections; using System.Collections.Generic; +using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Threading; namespace Ryujinx.Memory.Range { + public class RangeItem(TValue value) where TValue : IRange + { + public RangeItem Next; + public RangeItem Previous; + + public readonly ulong Address = value.Address; + public readonly ulong EndAddress = value.Address + value.Size; + + public readonly TValue Value = value; + + public readonly List QuickAccessAddresses = []; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool OverlapsWith(ulong address, ulong endAddress) + { + return Address < endAddress && address < EndAddress; + } + } + + class AddressEqualityComparer : IEqualityComparer + { + public bool Equals(ulong u1, ulong u2) + { + return u1 == u2; + } + + public int GetHashCode(ulong value) => (int)(value >> 5); + + public static readonly AddressEqualityComparer Comparer = new(); + } + + /// + /// Result of an Overlaps Finder function. WARNING: if the result is from the optimized + /// Overlaps Finder, the StartIndex will be -1 even when the result isn't empty + /// + /// + /// startIndex is inclusive. + /// endIndex is exclusive. + /// + public readonly struct OverlapResult where T : IRange + { + public readonly int StartIndex = -1; + public readonly int EndIndex = -1; + public readonly RangeItem QuickResult; + public int Count => EndIndex - StartIndex; + + public OverlapResult(int startIndex, int endIndex, RangeItem quickResult = null) + { + this.StartIndex = startIndex; + this.EndIndex = endIndex; + this.QuickResult = quickResult; + } + } + /// /// Sorted list of ranges that supports binary search. /// /// Type of the range. - public class RangeList : IEnumerable where T : IRange + public class RangeList : RangeListBase where T : IRange { - private readonly struct RangeItem where TValue : IRange - { - public readonly ulong Address; - public readonly ulong EndAddress; - - public readonly TValue Value; - - public RangeItem(TValue value) - { - Value = value; - - Address = value.Address; - EndAddress = value.Address + value.Size; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool OverlapsWith(ulong address, ulong endAddress) - { - return Address < endAddress && address < EndAddress; - } - } - - private const int BackingInitialSize = 1024; - private const int ArrayGrowthSize = 32; - - private RangeItem[] _items; - private readonly int _backingGrowthSize; - - public int Count { get; protected set; } + public readonly ReaderWriterLockSlim Lock = new(); + + private readonly Dictionary> _quickAccess = new(AddressEqualityComparer.Comparer); + /// + /// Creates a new range list. + /// + public RangeList() { } + /// /// Creates a new range list. /// /// The initial size of the backing array - public RangeList(int backingInitialSize = BackingInitialSize) - { - _backingGrowthSize = backingInitialSize; - _items = new RangeItem[backingInitialSize]; - } + public RangeList(int backingInitialSize) : base(backingInitialSize) { } /// /// Adds a new item to the list. /// /// The item to be added - public void Add(T item) + public override void Add(T item) { int index = BinarySearch(item.Address); @@ -72,27 +102,27 @@ namespace Ryujinx.Memory.Range /// /// The item to be updated /// True if the item was located and updated, false otherwise - public bool Update(T item) + protected override bool Update(T item) { int index = BinarySearch(item.Address); if (index >= 0) { - while (index > 0 && _items[index - 1].Address == item.Address) - { - index--; - } - while (index < Count) { - if (_items[index].Value.Equals(item)) + if (Items[index].Value.Equals(item)) { - _items[index] = new RangeItem(item); + foreach (ulong address in Items[index].QuickAccessAddresses) + { + _quickAccess.Remove(address); + } + + Items[index] = new RangeItem(item); return true; } - if (_items[index].Address > item.Address) + if (Items[index].Address > item.Address) { break; } @@ -107,23 +137,42 @@ namespace Ryujinx.Memory.Range [MethodImpl(MethodImplOptions.AggressiveInlining)] private void Insert(int index, RangeItem item) { - if (Count + 1 > _items.Length) + Debug.Assert(item.Address != item.EndAddress); + + Debug.Assert(item.Address % 32 == 0); + + if (Count + 1 > Items.Length) { - Array.Resize(ref _items, _items.Length + _backingGrowthSize); + Array.Resize(ref Items, Items.Length + BackingGrowthSize); } if (index >= Count) { if (index == Count) { - _items[Count++] = item; + if (index != 0) + { + item.Previous = Items[index - 1]; + Items[index - 1].Next = item; + } + Items[index] = item; + Count++; } } else { - Array.Copy(_items, index, _items, index + 1, Count - index); + Array.Copy(Items, index, Items, index + 1, Count - index); - _items[index] = item; + Items[index] = item; + if (index != 0) + { + item.Previous = Items[index - 1]; + Items[index - 1].Next = item; + } + + item.Next = Items[index + 1]; + Items[index + 1].Previous = item; + Count++; } } @@ -131,9 +180,71 @@ namespace Ryujinx.Memory.Range [MethodImpl(MethodImplOptions.AggressiveInlining)] private void RemoveAt(int index) { + foreach (ulong address in Items[index].QuickAccessAddresses) + { + _quickAccess.Remove(address); + } + + if (index < Count - 1) + { + Items[index + 1].Previous = index > 0 ? Items[index - 1] : null; + } + + if (index > 0) + { + Items[index - 1].Next = index < Count - 1 ? Items[index + 1] : null; + } + if (index < --Count) { - Array.Copy(_items, index + 1, _items, index, Count - index); + Array.Copy(Items, index + 1, Items, index, Count - index); + } + } + + /// + /// Removes a range of items from the item list + /// + /// The first item in the range of items to be removed + /// The last item in the range of items to be removed + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override void RemoveRange(RangeItem startItem, RangeItem endItem) + { + if (endItem.Next is not null) + { + endItem.Next.Previous = startItem.Previous; + } + + if (startItem.Previous is not null) + { + startItem.Previous.Next = endItem.Next; + } + + RangeItem current = startItem; + while (current != endItem.Next) + { + foreach (ulong address in current.QuickAccessAddresses) + { + _quickAccess.Remove(address); + } + + current = current.Next; + } + + RangeItem[] array = []; + OverlapResult overlapResult = FindOverlaps(startItem.Address, endItem.EndAddress, ref array); + + if (overlapResult.EndIndex < Count) + { + Array.Copy(Items, overlapResult.EndIndex, Items, overlapResult.StartIndex, Count - overlapResult.EndIndex); + Count -= overlapResult.Count; + } + else if (overlapResult.EndIndex == Count) + { + Count = overlapResult.StartIndex; + } + else + { + Debug.Assert(false); } } @@ -142,27 +253,22 @@ namespace Ryujinx.Memory.Range /// /// The item to be removed /// True if the item was removed, or false if it was not found - public bool Remove(T item) + public override bool Remove(T item) { int index = BinarySearch(item.Address); if (index >= 0) { - while (index > 0 && _items[index - 1].Address == item.Address) - { - index--; - } - while (index < Count) { - if (_items[index].Value.Equals(item)) + if (Items[index].Value.Equals(item)) { RemoveAt(index); return true; } - if (_items[index].Address > item.Address) + if (Items[index].Address > item.Address) { break; } @@ -173,310 +279,130 @@ namespace Ryujinx.Memory.Range return false; } - + /// - /// Updates an item's end address. - /// - /// The item to be updated - public void UpdateEndAddress(T item) - { - int index = BinarySearch(item.Address); - - if (index >= 0) - { - while (index > 0 && _items[index - 1].Address == item.Address) - { - index--; - } - - while (index < Count) - { - if (_items[index].Value.Equals(item)) - { - _items[index] = new RangeItem(item); - - return; - } - - if (_items[index].Address > item.Address) - { - break; - } - - index++; - } - } - } - - /// - /// Gets the first item on the list overlapping in memory with the specified item. + /// Gets an item on the list overlapping the specified memory range. /// /// - /// Despite the name, this has no ordering guarantees of the returned item. - /// It only ensures that the item returned overlaps the specified item. - /// - /// Item to check for overlaps - /// The overlapping item, or the default value for the type if none found - public T FindFirstOverlap(T item) - { - return FindFirstOverlap(item.Address, item.Size); - } - - /// - /// Gets the first item on the list overlapping the specified memory range. - /// - /// - /// Despite the name, this has no ordering guarantees of the returned item. + /// This has no ordering guarantees of the returned item. /// It only ensures that the item returned overlaps the specified memory range. /// /// Start address of the range /// Size in bytes of the range /// The overlapping item, or the default value for the type if none found - public T FindFirstOverlap(ulong address, ulong size) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override RangeItem FindOverlap(ulong address, ulong size) { + int index = BinarySearchLeftEdge(address, address + size); + + if (index < 0) + { + return null; + } + + return Items[index]; + } + + /// + /// Gets an item on the list overlapping the specified memory range. + /// + /// + /// This has no ordering guarantees of the returned item. + /// It only ensures that the item returned overlaps the specified memory range. + /// + /// Start address of the range + /// Size in bytes of the range + /// The overlapping item, or the default value for the type if none found + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public override RangeItem FindOverlapFast(ulong address, ulong size) + { + if (_quickAccess.TryGetValue(address, out RangeItem quickResult)) + { + return quickResult; + } + int index = BinarySearch(address, address + size); if (index < 0) { - return default; + return null; } - return _items[index].Value; - } + if (Items[index].OverlapsWith(address, address + 1)) + { + _quickAccess.Add(address, Items[index]); + Items[index].QuickAccessAddresses.Add(address); + } - /// - /// Gets all items overlapping with the specified item in memory. - /// - /// Item to check for overlaps - /// Output array where matches will be written. It is automatically resized to fit the results - /// The number of overlapping items found - public int FindOverlaps(T item, ref T[] output) - { - return FindOverlaps(item.Address, item.Size, ref output); + return Items[index]; } - + /// /// Gets all items on the list overlapping the specified memory range. /// /// Start address of the range /// Size in bytes of the range /// Output array where matches will be written. It is automatically resized to fit the results - /// The number of overlapping items found - public int FindOverlaps(ulong address, ulong size, ref T[] output) + /// Range information of overlapping items found + private OverlapResult FindOverlaps(ulong address, ulong size, ref RangeItem[] output) { - int outputIndex = 0; + int outputCount = 0; ulong endAddress = address + size; + + int startIndex = BinarySearch(address, endAddress); + if (startIndex < 0) + startIndex = ~startIndex; + int endIndex = -1; - for (int i = 0; i < Count; i++) + for (int i = startIndex; i < Count; i++) { - ref RangeItem item = ref _items[i]; + ref RangeItem item = ref Items[i]; if (item.Address >= endAddress) { + endIndex = i; break; } if (item.OverlapsWith(address, endAddress)) { - if (outputIndex == output.Length) - { - Array.Resize(ref output, outputIndex + ArrayGrowthSize); - } - - output[outputIndex++] = item.Value; + outputCount++; } } - return outputIndex; - } - - /// - /// Gets all items overlapping with the specified item in memory. - /// - /// - /// This method only returns correct results if none of the items on the list overlaps with - /// each other. If that is not the case, this method should not be used. - /// This method is faster than the regular method to find all overlaps. - /// - /// Item to check for overlaps - /// Output array where matches will be written. It is automatically resized to fit the results - /// The number of overlapping items found - public int FindOverlapsNonOverlapping(T item, ref T[] output) - { - return FindOverlapsNonOverlapping(item.Address, item.Size, ref output); - } - - /// - /// Gets all items on the list overlapping the specified memory range. - /// - /// - /// This method only returns correct results if none of the items on the list overlaps with - /// each other. If that is not the case, this method should not be used. - /// This method is faster than the regular method to find all overlaps. - /// - /// Start address of the range - /// Size in bytes of the range - /// Output array where matches will be written. It is automatically resized to fit the results - /// The number of overlapping items found - public int FindOverlapsNonOverlapping(ulong address, ulong size, ref T[] output) - { - // This is a bit faster than FindOverlaps, but only works - // when none of the items on the list overlaps with each other. - int outputIndex = 0; - - ulong endAddress = address + size; - - int index = BinarySearch(address, endAddress); - - if (index >= 0) + if (endIndex == -1 && outputCount > 0) { - while (index > 0 && _items[index - 1].OverlapsWith(address, endAddress)) - { - index--; - } - - do - { - if (outputIndex == output.Length) - { - Array.Resize(ref output, outputIndex + ArrayGrowthSize); - } - - output[outputIndex++] = _items[index++].Value; - } - while (index < Count && _items[index].OverlapsWith(address, endAddress)); + endIndex = Count; } - return outputIndex; - } - - /// - /// Gets all items on the list with the specified memory address. - /// - /// Address to find - /// Output array where matches will be written. It is automatically resized to fit the results - /// The number of matches found - public int FindOverlaps(ulong address, ref T[] output) - { - int index = BinarySearch(address); - - int outputIndex = 0; - - if (index >= 0) + if (outputCount > 0 && outputCount == endIndex - startIndex) { - while (index > 0 && _items[index - 1].Address == address) - { - index--; - } - - while (index < Count) - { - ref RangeItem overlap = ref _items[index++]; - - if (overlap.Address != address) - { - break; - } - - if (outputIndex == output.Length) - { - Array.Resize(ref output, outputIndex + ArrayGrowthSize); - } - - output[outputIndex++] = overlap.Value; - } + Array.Resize(ref output, outputCount); + Array.Copy(Items, endIndex - outputCount, output, 0, outputCount); + + return new OverlapResult(startIndex, endIndex); } - - return outputIndex; - } - - /// - /// Performs binary search on the internal list of items. - /// - /// Address to find - /// List index of the item, or complement index of nearest item with lower value on the list - private int BinarySearch(ulong address) - { - int left = 0; - int right = Count - 1; - - while (left <= right) + else if (outputCount > 0) { - int range = right - left; - - int middle = left + (range >> 1); - - ref RangeItem item = ref _items[middle]; - - if (item.Address == address) + Array.Resize(ref output, outputCount); + int arrIndex = 0; + for (int i = startIndex; i < endIndex; i++) { - return middle; - } - - if (address < item.Address) - { - right = middle - 1; - } - else - { - left = middle + 1; + output[arrIndex++] = Items[i]; } + + return new OverlapResult(endIndex - outputCount, endIndex); } - - return ~left; + + return new OverlapResult(); } - /// - /// Performs binary search for items overlapping a given memory range. - /// - /// Start address of the range - /// End address of the range - /// List index of the item, or complement index of nearest item with lower value on the list - private int BinarySearch(ulong address, ulong endAddress) - { - int left = 0; - int right = Count - 1; - - while (left <= right) - { - int range = right - left; - - int middle = left + (range >> 1); - - ref RangeItem item = ref _items[middle]; - - if (item.OverlapsWith(address, endAddress)) - { - return middle; - } - - if (address < item.Address) - { - right = middle - 1; - } - else - { - left = middle + 1; - } - } - - return ~left; - } - - public IEnumerator GetEnumerator() + public override IEnumerator GetEnumerator() { for (int i = 0; i < Count; i++) { - yield return _items[i].Value; - } - } - - IEnumerator IEnumerable.GetEnumerator() - { - for (int i = 0; i < Count; i++) - { - yield return _items[i].Value; + yield return Items[i].Value; } } } diff --git a/src/Ryujinx.Memory/Range/RangeListBase.cs b/src/Ryujinx.Memory/Range/RangeListBase.cs new file mode 100644 index 000000000..7e26442f0 --- /dev/null +++ b/src/Ryujinx.Memory/Range/RangeListBase.cs @@ -0,0 +1,359 @@ +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Memory.Range +{ + public abstract class RangeListBase : IEnumerable where T : IRange + { + protected const int BackingInitialSize = 1024; + + protected RangeItem[] Items; + protected readonly int BackingGrowthSize; + + public int Count { get; protected set; } + + /// + /// Creates a new range list. + /// + /// The initial size of the backing array + protected RangeListBase(int backingInitialSize = BackingInitialSize) + { + BackingGrowthSize = backingInitialSize; + Items = new RangeItem[backingInitialSize]; + } + + public abstract void Add(T item); + + /// + /// Updates an item's end address on the list. Address must be the same. + /// + /// The item to be updated + /// True if the item was located and updated, false otherwise + protected abstract bool Update(T item); + + public abstract bool Remove(T item); + + public abstract void RemoveRange(RangeItem startItem, RangeItem endItem); + + public abstract RangeItem FindOverlap(ulong address, ulong size); + + public abstract RangeItem FindOverlapFast(ulong address, ulong size); + + /// + /// Performs binary search on the internal list of items. + /// + /// Address to find + /// List index of the item, or complement index of nearest item with lower value on the list + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected int BinarySearch(ulong address) + { + int left = 0; + int right = Count - 1; + + while (left <= right) + { + int range = right - left; + + int middle = left + (range >> 1); + + ref RangeItem item = ref Items[middle]; + + if (item.Address == address) + { + return middle; + } + + if (address < item.Address) + { + right = middle - 1; + } + else + { + left = middle + 1; + } + } + + return ~left; + } + + /// + /// Performs binary search for items overlapping a given memory range. + /// + /// Start address of the range + /// End address of the range + /// List index of the item, or complement index of nearest item with lower value on the list + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected int BinarySearch(ulong address, ulong endAddress) + { + int left = 0; + int right = Count - 1; + + while (left <= right) + { + int range = right - left; + + int middle = left + (range >> 1); + + ref RangeItem item = ref Items[middle]; + + if (item.OverlapsWith(address, endAddress)) + { + return middle; + } + + if (address < item.Address) + { + right = middle - 1; + } + else + { + left = middle + 1; + } + } + + return ~left; + } + + /// + /// Performs binary search for items overlapping a given memory range. + /// + /// Start address of the range + /// End address of the range + /// List index of the item, or complement index of nearest item with lower value on the list + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected int BinarySearchLeftEdge(ulong address, ulong endAddress) + { + if (Count == 0) + return ~0; + + int left = 0; + int right = Count - 1; + + while (left <= right) + { + int range = right - left; + + int middle = left + (range >> 1); + + ref RangeItem item = ref Items[middle]; + + bool match = item.OverlapsWith(address, endAddress); + + if (range == 0) + { + if (match) + return middle; + else if (address < item.Address) + return ~(right); + else + return ~(right + 1); + } + + if (match) + { + right = middle; + } + else if (address < item.Address) + { + right = middle - 1; + } + else + { + left = middle + 1; + } + } + + return ~left; + } + + /// + /// Performs binary search for items overlapping a given memory range. + /// + /// Start address of the range + /// End address of the range + /// List index of the item, or complement index of nearest item with lower value on the list + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected int BinarySearchRightEdge(ulong address, ulong endAddress) + { + if (Count == 0) + return ~0; + + int left = 0; + int right = Count - 1; + + while (left <= right) + { + int range = right - left; + + int middle = right - (range >> 1); + + ref RangeItem item = ref Items[middle]; + + bool match = item.OverlapsWith(address, endAddress); + + if (range == 0) + { + if (match) + return middle; + else if (endAddress > item.EndAddress) + return ~(left + 1); + else + return ~(left); + } + + if (match) + { + left = middle; + } + else if (address < item.Address) + { + right = middle - 1; + } + else + { + left = middle + 1; + } + } + + return ~left; + } + + /// + /// Performs binary search for items overlapping a given memory range. + /// + /// Start address of the range + /// End address of the range + /// Range information (inclusive, exclusive) of items that overlaps, or complement index of nearest item with lower value on the list + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected (int, int) BinarySearchEdges(ulong address, ulong endAddress) + { + if (Count == 0) + return (~0, ~0); + + if (Count == 1) + { + ref RangeItem item = ref Items[0]; + + if (item.OverlapsWith(address, endAddress)) + { + return (0, 1); + } + + if (address < item.Address) + { + return (~0, ~0); + } + else + { + return (~1, ~1); + } + } + + int left = 0; + int right = Count - 1; + + int leftEdge = -1; + int rightEdgeMatch = -1; + int rightEdgeNoMatch = -1; + + while (left <= right) + { + int range = right - left; + + int middle = left + (range >> 1); + + ref RangeItem item = ref Items[middle]; + + bool match = item.OverlapsWith(address, endAddress); + + if (range == 0) + { + if (match) + { + leftEdge = middle; + break; + } + else if (address < item.Address) + { + return (~right, ~right); + } + else + { + return (~(right + 1), ~(right + 1)); + } + } + + if (match) + { + right = middle; + if (rightEdgeMatch == -1) + rightEdgeMatch = middle; + } + else if (address < item.Address) + { + right = middle - 1; + rightEdgeNoMatch = middle; + } + else + { + left = middle + 1; + } + } + + if (left > right) + { + return (~left, ~left); + } + + if (rightEdgeMatch == -1) + { + return (leftEdge, leftEdge + 1); + } + + left = rightEdgeMatch; + right = rightEdgeNoMatch > 0 ? rightEdgeNoMatch : Count - 1; + + while (left <= right) + { + int range = right - left; + + int middle = right - (range >> 1); + + ref RangeItem item = ref Items[middle]; + + bool match = item.OverlapsWith(address, endAddress); + + if (range == 0) + { + if (match) + return (leftEdge, middle + 1); + else + return (leftEdge, middle); + } + + if (match) + { + left = middle; + } + else if (address < item.Address) + { + right = middle - 1; + } + else + { + left = middle + 1; + } + } + + return (leftEdge, right + 1); + } + + public abstract IEnumerator GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Ryujinx.Memory/Tracking/MemoryTracking.cs b/src/Ryujinx.Memory/Tracking/MemoryTracking.cs index e7791fec3..8f7ef0be2 100644 --- a/src/Ryujinx.Memory/Tracking/MemoryTracking.cs +++ b/src/Ryujinx.Memory/Tracking/MemoryTracking.cs @@ -1,4 +1,3 @@ -using Ryujinx.Common.Pools; using Ryujinx.Memory.Range; using System.Collections.Generic; @@ -76,17 +75,16 @@ namespace Ryujinx.Memory.Tracking lock (TrackingLock) { - ref VirtualRegion[] overlaps = ref ThreadStaticArray.Get(); - for (int type = 0; type < 2; type++) { NonOverlappingRangeList regions = type == 0 ? _virtualRegions : _guestVirtualRegions; - - int count = regions.FindOverlapsNonOverlapping(va, size, ref overlaps); - - for (int i = 0; i < count; i++) + regions.Lock.EnterReadLock(); + (RangeItem first, RangeItem last) = regions.FindOverlaps(va, size); + + RangeItem current = first; + while (last != null && current != last.Next) { - VirtualRegion region = overlaps[i]; + VirtualRegion region = current.Value; // If the region has been fully remapped, signal that it has been mapped again. bool remapped = _memoryManager.IsRangeMapped(region.Address, region.Size); @@ -96,7 +94,9 @@ namespace Ryujinx.Memory.Tracking } region.UpdateProtection(); + current = current.Next; } + regions.Lock.ExitReadLock(); } } } @@ -114,20 +114,21 @@ namespace Ryujinx.Memory.Tracking lock (TrackingLock) { - ref VirtualRegion[] overlaps = ref ThreadStaticArray.Get(); - for (int type = 0; type < 2; type++) { NonOverlappingRangeList regions = type == 0 ? _virtualRegions : _guestVirtualRegions; - - int count = regions.FindOverlapsNonOverlapping(va, size, ref overlaps); - - for (int i = 0; i < count; i++) + regions.Lock.EnterReadLock(); + (RangeItem first, RangeItem last) = regions.FindOverlaps(va, size); + + RangeItem current = first; + while (last != null && current != last.Next) { - VirtualRegion region = overlaps[i]; + VirtualRegion region = current.Value; region.SignalMappingChanged(false); + current = current.Next; } + regions.Lock.ExitReadLock(); } } } @@ -165,10 +166,11 @@ namespace Ryujinx.Memory.Tracking /// A list of virtual regions within the given range internal List GetVirtualRegionsForHandle(ulong va, ulong size, bool guest) { - List result = []; NonOverlappingRangeList regions = guest ? _guestVirtualRegions : _virtualRegions; - regions.GetOrAddRegions(result, va, size, (va, size) => new VirtualRegion(this, va, size, guest)); - + regions.Lock.EnterUpgradeableReadLock(); + regions.GetOrAddRegions(out List result, va, size, (va, size) => new VirtualRegion(this, va, size, guest)); + regions.Lock.ExitUpgradeableReadLock(); + return result; } @@ -296,25 +298,33 @@ namespace Ryujinx.Memory.Tracking lock (TrackingLock) { - ref VirtualRegion[] overlaps = ref ThreadStaticArray.Get(); - NonOverlappingRangeList regions = guest ? _guestVirtualRegions : _virtualRegions; + List> overlaps = []; + + // We use the non-span method here because keeping the lock will cause a deadlock. + regions.Lock.EnterReadLock(); + (RangeItem first, RangeItem last) = regions.FindOverlaps(address, size); + + RangeItem current = first; + while (last != null && current != last.Next) + { + overlaps.Add(current); + current = current.Next; + } + regions.Lock.ExitReadLock(); - int count = regions.FindOverlapsNonOverlapping(address, size, ref overlaps); - - if (count == 0 && !precise) + if (first is null && !precise) { if (_memoryManager.IsRangeMapped(address, size)) { // TODO: There is currently the possibility that a page can be protected after its virtual region is removed. // This code handles that case when it happens, but it would be better to find out how this happens. _memoryManager.TrackingReprotect(address & ~(ulong)(_pageSize - 1), (ulong)_pageSize, MemoryPermission.ReadAndWrite, guest); + return true; // This memory _should_ be mapped, so we need to try again. } - else - { - shouldThrow = true; - } + + shouldThrow = true; } else { @@ -324,9 +334,9 @@ namespace Ryujinx.Memory.Tracking size += (ulong)_pageSize; } - for (int i = 0; i < count; i++) + for (int i = 0; i < overlaps.Count; i++) { - VirtualRegion region = overlaps[i]; + VirtualRegion region = overlaps[i].Value; if (precise) { From bb06eb751ba8b7316a55d91fa4617d218dfb1f36 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Wed, 6 Aug 2025 18:36:57 -0500 Subject: [PATCH 07/24] Revert "fix: Super Mario Party Jamboree audio renderer crashing" This reverts commit c0c021c7a966e32ed39018f8ec00f9f373173b60. This commit was useless, and submitted by a GDKchan-obsessed chronically online lunatic who has disrespected the maintainers of this fork due to petty disagreements of how we run our Discord server. This is my parting gift to you: Stay gone. I'd prefer this code the way it was, because then you didn't touch it. For the record, this commit is literally useless. The behavioral outcome is functionally identical to before the commit. --- src/Ryujinx.Audio/Renderer/Dsp/AdpcmHelper.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Ryujinx.Audio/Renderer/Dsp/AdpcmHelper.cs b/src/Ryujinx.Audio/Renderer/Dsp/AdpcmHelper.cs index 1a5802a37..8b497fe2a 100644 --- a/src/Ryujinx.Audio/Renderer/Dsp/AdpcmHelper.cs +++ b/src/Ryujinx.Audio/Renderer/Dsp/AdpcmHelper.cs @@ -81,14 +81,14 @@ namespace Ryujinx.Audio.Renderer.Dsp [MethodImpl(MethodImplOptions.AggressiveInlining)] private static short GetCoefficientAtIndex(ReadOnlySpan coefficients, int index) { - if ((uint)index < (uint)coefficients.Length) + if ((uint)index >= (uint)coefficients.Length) { - return coefficients[index]; + Logger.Error?.Print(LogClass.AudioRenderer, $"Out of bound read for coefficient at index {index}"); + + return 0; } - Logger.Error?.Print(LogClass.AudioRenderer, $"Out of bound read for coefficient at index {index}"); - - return 0; + return coefficients[index]; } [MethodImpl(MethodImplOptions.AggressiveInlining)] From 01e1cd4d5a13c2de13fb1d0cd7384e89cfaece73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=9C=A8=E4=B8=AD=E5=9B=BD=E7=9A=84=E6=B3=B0=E5=9B=BD?= =?UTF-8?q?=E9=9D=92=E5=B9=B4=5F?= Date: Fri, 8 Aug 2025 17:34:56 +0800 Subject: [PATCH 08/24] update thai language in locales.json (ryubing/ryujinx!102) See merge request ryubing/ryujinx!102 --- assets/locales.json | 630 ++++++++++++++++++++++---------------------- 1 file changed, 315 insertions(+), 315 deletions(-) diff --git a/assets/locales.json b/assets/locales.json index 9a1361218..3ac0035a5 100644 --- a/assets/locales.json +++ b/assets/locales.json @@ -110,7 +110,7 @@ "pt_BR": "Editor de Mii", "ru_RU": "Апплет Mii Editor", "sv_SE": "Redigera Mii-applet", - "th_TH": "", + "th_TH": "ตัวแก้ไขตัวละคร Mii", "tr_TR": "", "uk_UA": "Аплет редагування Mii", "zh_CN": "Mii 小程序", @@ -485,7 +485,7 @@ "pt_BR": "Abrir Pasta de Capturas de Tela", "ru_RU": "Открыть папку со скриншотами", "sv_SE": "Öppna skärmbildsmappen", - "th_TH": "", + "th_TH": "เปิดโฟลเดอร์ที่เก็บภาพหน้าจอ", "tr_TR": "", "uk_UA": "Відкрити теку скріншотів", "zh_CN": "打开截图文件夹", @@ -635,7 +635,7 @@ "pt_BR": "Iniciar Jogos Ocultando a Interface", "ru_RU": "Запускать игры скрывая интерфейс", "sv_SE": "Starta spel med dolt användargränssnitt", - "th_TH": "", + "th_TH": "เริ่มเกมโดยซ่อน UI", "tr_TR": "", "uk_UA": "Запускати ігри з прихованим інтерфейсом", "zh_CN": "启动游戏时隐藏 UI", @@ -735,7 +735,7 @@ "pt_BR": "_Ações", "ru_RU": "_Действия", "sv_SE": "_Åtgärder", - "th_TH": "การดำเนินการ", + "th_TH": "_การดำเนินการ", "tr_TR": "_Eylemler", "uk_UA": "_Дії", "zh_CN": "操作(_A)", @@ -760,7 +760,7 @@ "pt_BR": "Simular Mensagem de Acordar o Console", "ru_RU": "Имитировать сообщение пробуждения", "sv_SE": "Simulera uppvakningsmeddelande", - "th_TH": "จำลองข้อความปลุก", + "th_TH": "จำลองการปลุกอุปกรณ์ให้ทำงาน", "tr_TR": "Uyandırma Mesajı Simüle Et", "uk_UA": "Симулювати повідомлення про пробудження", "zh_CN": "模拟唤醒消息", @@ -810,7 +810,7 @@ "pt_BR": "Escaneie um Amiibo (de um .bin)", "ru_RU": "Сканировать Amiibo (из папки Bin)", "sv_SE": "Skanna en Amiibo (från bin-fil)", - "th_TH": "", + "th_TH": "สแกนอามีโบ (จากไฟล์ Bin)", "tr_TR": "", "uk_UA": "Сканувати Amiibo (з теки Bin)", "zh_CN": "扫描 Amiibo (从 bin 文件)", @@ -910,7 +910,7 @@ "pt_BR": "Instalar Chaves", "ru_RU": "Установить ключи", "sv_SE": "Installera nycklar", - "th_TH": "", + "th_TH": "ติดตั้งคีย์", "tr_TR": "", "uk_UA": "Встановити ключі", "zh_CN": "安装密匙", @@ -935,7 +935,7 @@ "pt_BR": "Instalar chaves de CHAVES ou ZIP", "ru_RU": "Установить ключи из файла KEYS или ZIP", "sv_SE": "Installera nycklar från KEYS eller ZIP", - "th_TH": "", + "th_TH": "ติดตั้งคีย์จากไฟล์ KEYS หรือ ZIP", "tr_TR": "", "uk_UA": "Встановити ключі з файлу .KEYS або .ZIP", "zh_CN": "从 .KEYS 文件或 .ZIP 压缩包安装密匙", @@ -960,7 +960,7 @@ "pt_BR": "Instalar chaves de um diretório", "ru_RU": "Установить ключи из папки", "sv_SE": "Installera nycklar från en katalog", - "th_TH": "", + "th_TH": "ติดตั้งคีย์จากไดเรกทอรี", "tr_TR": "", "uk_UA": "Встановити ключі з теки", "zh_CN": "从一个文件夹安装密匙", @@ -1060,7 +1060,7 @@ "pt_BR": "Reduzir Arquivos XCI", "ru_RU": "Уменьшить размер XCI файлов", "sv_SE": "Optimera XCI-filer", - "th_TH": "", + "th_TH": "ตัดแต่งไฟล์ XCI", "tr_TR": "", "uk_UA": "Обрізати XCI файли", "zh_CN": "瘦身 XCI 文件", @@ -1285,7 +1285,7 @@ "pt_BR": "FAQ & Guias", "ru_RU": "FAQ и Руководства", "sv_SE": "Frågor, svar och guider", - "th_TH": "", + "th_TH": "คำถามที่พบบ่อย & คู่มือ", "tr_TR": "", "uk_UA": "FAQ та посібники", "zh_CN": "问答与指南", @@ -1310,7 +1310,7 @@ "pt_BR": "FAQ e Solução de Problemas", "ru_RU": "FAQ & Устранение неполадок", "sv_SE": "Frågor, svar och felsökningssida", - "th_TH": "", + "th_TH": "หน้าคำถามที่พบบ่อยและแก้ไขปัญหา", "tr_TR": "", "uk_UA": "FAQ та усунення несправностей (eng)", "zh_CN": "常见问题和问题排除页面", @@ -1335,7 +1335,7 @@ "pt_BR": "Abre a página de FAQ e solução de problemas no wiki oficial do Ryujinx", "ru_RU": "Открывает страницы с FAQ и Устранением неполадок на официальной странице вики Ryujinx", "sv_SE": "Öppnar Frågor, svar och felsökningssidan på den officiella Ryujinx-wikin", - "th_TH": "", + "th_TH": "เปิดหน้ารวมคำถามที่พบบ่อยและวิธีแก้ไขปัญหาบนเว็บไซต์วิกิของ Ryujinx", "tr_TR": "", "uk_UA": "Відкриває сторінку з Посібником по усуненню помилок та несправностей на офіційній вікі-сторінці Ryujinx (англійською)", "zh_CN": "打开 Ryujinx 官方 Wiki 的常见问题和问题排除页面", @@ -1360,7 +1360,7 @@ "pt_BR": "Guia de Instalação e Configuração", "ru_RU": "Руководство по установке и настройке", "sv_SE": "Konfigurationsguide", - "th_TH": "", + "th_TH": "คู่มือการติดตั้งและปรับแต่งระบบ", "tr_TR": "", "uk_UA": "Посібник зі встановлення та налаштування (eng)", "zh_CN": "安装与配置指南", @@ -1385,7 +1385,7 @@ "pt_BR": "Abre o guia de instalação e configuração no wiki oficial do Ryujinx", "ru_RU": "Открывает страницу Руководство по установке и настройке на официальной странице вики Ryujinx", "sv_SE": "Öppnar konfigurationsguiden på den officiella Ryujinx-wikin", - "th_TH": "", + "th_TH": "เปิดคู่มือการติดตั้งและตั้งค่าบนวิกิอย่างเป็นทางการของ Ryujinx", "tr_TR": "", "uk_UA": "Відкриває посібник з Налаштування та конфігурації на офіційній вікі-сторінці Ryujinx (англійською)", "zh_CN": "打开 Ryujinx 官方 Wiki 的安装与配置指南", @@ -1410,7 +1410,7 @@ "pt_BR": "Guia Multijogador (LDN/LAN)", "ru_RU": "Гайд по мультиплееру (LDN/LAN)", "sv_SE": "Flerspelarguide (LDN/LAN)", - "th_TH": "", + "th_TH": "คู่มือการเล่นหลายคนผ่านระบบ LDN หรือ LAN", "tr_TR": "", "uk_UA": "Посібник з мультиплеєру (LDN/LAN) (eng)", "zh_CN": "多人游戏(LDN/LAN)指南", @@ -1435,7 +1435,7 @@ "pt_BR": "Abre o guia multijogador no wiki oficial do Ryujinx", "ru_RU": "Открывает гайд по мультиплееру на официальной странице вики Ryujinx", "sv_SE": "Öppnar flerspelarguiden på den officiella Ryujinx-wikin", - "th_TH": "", + "th_TH": "เปิดคู่มือการเล่นหลายคนบนวิกิอย่างเป็นทางการของ Ryujinx", "tr_TR": "", "uk_UA": "Відкриває посібник з налаштування Мультиплеєру на офіційній вікі-сторінці Ryujinx (англійською)", "zh_CN": "打开 Ryujinx 官方 Wiki 的多人游戏指南", @@ -1585,7 +1585,7 @@ "pt_BR": "Desenvolvido por {0}", "ru_RU": "Разработана {0}", "sv_SE": "Utvecklat av {0}", - "th_TH": "", + "th_TH": "พัฒนาโดย {0}", "tr_TR": "", "uk_UA": "Розроблено: {0}", "zh_CN": "由 {0} 开发", @@ -1635,7 +1635,7 @@ "pt_BR": "Tempo de Jogo:", "ru_RU": "Время в игре:", "sv_SE": "Speltid:", - "th_TH": "เล่นไปแล้ว:", + "th_TH": "เวลาที่เล่น:", "tr_TR": "Oynama Süresi:", "uk_UA": "Зіграно часу:", "zh_CN": "游玩时长:", @@ -1660,7 +1660,7 @@ "pt_BR": "Última vez Jogado:", "ru_RU": "Последний запуск:", "sv_SE": "Senast spelad:", - "th_TH": "เล่นล่าสุด:", + "th_TH": "เล่นไปล่าสุด:", "tr_TR": "Son Oynama Tarihi:", "uk_UA": "Востаннє зіграно:", "zh_CN": "最近游玩:", @@ -1760,7 +1760,7 @@ "pt_BR": "Tempo de Jogo", "ru_RU": "Время в игре", "sv_SE": "Speltid", - "th_TH": "เล่นไปแล้ว", + "th_TH": "เวลาที่เล่น", "tr_TR": "Oynama Süresi", "uk_UA": "Зіграно часу", "zh_CN": "游玩时长", @@ -1935,7 +1935,7 @@ "pt_BR": "Status: Desativado", "ru_RU": "Статус: Отключено", "sv_SE": "Status: Inaktiverad", - "th_TH": "", + "th_TH": "สถานะ: ถูกปิดใช้งาน", "tr_TR": "Durum: Devre Dışı", "uk_UA": "Статус: Вимкнено", "zh_CN": "状态:禁用", @@ -1960,7 +1960,7 @@ "pt_BR": "Status: Crescente", "ru_RU": "Статус: По возрастанию", "sv_SE": "Status: Stigande", - "th_TH": "สถานะ: เพิ่มขึ้น", + "th_TH": "สถานะ: เรียงจากน้อยไปมาก", "tr_TR": "Durum: Artan", "uk_UA": "Статус: Зростання", "zh_CN": "状态:升序", @@ -1985,7 +1985,7 @@ "pt_BR": "Status: Decrescente", "ru_RU": "Статус: По Убыванию", "sv_SE": "Status: Fallande", - "th_TH": "สถานะ: ลดลง", + "th_TH": "สถานะ: เรียงจากมากไปน้อย", "tr_TR": "Durum: Azalan", "uk_UA": "Статус: Зменшення", "zh_CN": "状态:降序", @@ -2010,7 +2010,7 @@ "pt_BR": "Compatibilidade:", "ru_RU": "Совместимость:", "sv_SE": "Kompatibilitet:", - "th_TH": "", + "th_TH": "ความเข้ากันได้:", "tr_TR": "", "uk_UA": "Сумісність:", "zh_CN": "兼容性:", @@ -2035,7 +2035,7 @@ "pt_BR": "ID do Título:", "ru_RU": "ID приложения", "sv_SE": "Titel-id:", - "th_TH": "", + "th_TH": "รหัสเกม:", "tr_TR": "", "uk_UA": "ID гри:", "zh_CN": "标题 ID:", @@ -2060,7 +2060,7 @@ "pt_BR": "Jogos Hospedados: {0}", "ru_RU": "Запущенно игр: {0}", "sv_SE": "Värdskap för spel: {0}", - "th_TH": "", + "th_TH": "เกมที่โฮสต์: {0}", "tr_TR": "", "uk_UA": "Розміщені ігри: {0}", "zh_CN": "服务的游戏: {0}", @@ -2085,7 +2085,7 @@ "pt_BR": "Jogadores Online: {0}", "ru_RU": "Игроков онлайн: {0}", "sv_SE": "Online-spelare: {0}", - "th_TH": "", + "th_TH": "จำนวนผู้เล่นออนไลน์: {0}", "tr_TR": "", "uk_UA": "Гравців онлайн: {0}", "zh_CN": "在线玩家: {0}", @@ -2110,7 +2110,7 @@ "pt_BR": "Tempo total de jogo: {0}", "ru_RU": "", "sv_SE": "Total speltid: {0}", - "th_TH": "", + "th_TH": "เวลาที่เล่นทั้งหมด: {0}", "tr_TR": "Toplam Oyun Süresi: {0}", "uk_UA": "Всього зіграно: {0}", "zh_CN": "总游戏时间: {0}", @@ -2135,7 +2135,7 @@ "pt_BR": "Abrir Diretório de Saves do Usuário", "ru_RU": "Открыть папку с сохранениями", "sv_SE": "Öppna användarkatalog för sparningar", - "th_TH": "เปิดไดเร็กทอรี่บันทึกของผู้ใช้", + "th_TH": "เปิดโฟลเดอร์บันทึกข้อมูลผู้ใช้", "tr_TR": "Kullanıcı Kayıt Dosyası Dizinini Aç", "uk_UA": "Відкрити теку збережень користувача", "zh_CN": "打开用户存档目录", @@ -2235,7 +2235,7 @@ "pt_BR": "Gerenciar DLCs", "ru_RU": "Управление DLC", "sv_SE": "Hantera DLC", - "th_TH": "จัดการ DLC", + "th_TH": "จัดการเนื้อหาเสริม (DLC)", "tr_TR": "DLC'leri Yönet", "uk_UA": "Керування DLC", "zh_CN": "管理 DLC", @@ -2285,7 +2285,7 @@ "pt_BR": "Reconstruir Cache PPTC", "ru_RU": "Перестроить очередь PPTC", "sv_SE": "Kölägg PPTC Rebuild", - "th_TH": "เพิ่มคิวการสร้าง PPTC ใหม่", + "th_TH": "เพิ่มงานสร้าง PPTC ใหม่ลงในคิว", "tr_TR": "PPTC Yeniden Yapılandırmasını Başlat", "uk_UA": "Додати до черги перекомпіляцію PPTC", "zh_CN": "清除 PPTC 缓存文件", @@ -2335,7 +2335,7 @@ "pt_BR": "Limpar Cache PPTC", "ru_RU": "Очистить кэш PPTC", "sv_SE": "Rensa PPTC-cache", - "th_TH": "", + "th_TH": "ล้างแคช PPTC", "tr_TR": "", "uk_UA": "Очистити кеш PPTC", "zh_CN": "清理 PPTC 缓存", @@ -2360,7 +2360,7 @@ "pt_BR": "Limpar Cache de Shader", "ru_RU": "Очистить кэш шейдеров", "sv_SE": "Töm shader cache", - "th_TH": "ล้างแคช แสงเงา", + "th_TH": "ล้างแคชเชเดอร์", "tr_TR": "Shader Önbelleğini Temizle", "uk_UA": "Очистити кеш шейдерів", "zh_CN": "清除着色器缓存文件", @@ -2385,7 +2385,7 @@ "pt_BR": "Abrir Diretório de Cache PPTC", "ru_RU": "Открыть папку PPTC", "sv_SE": "Öppna PPTC-katalog", - "th_TH": "เปิดไดเรกทอรี่ PPTC", + "th_TH": "เปิดไดเรกทอรี PPTC", "tr_TR": "PPTC Dizinini Aç", "uk_UA": "Відкрити теку PPTC", "zh_CN": "打开 PPTC 缓存目录", @@ -2410,7 +2410,7 @@ "pt_BR": "Abrir Diretório do Cache de Shaders", "ru_RU": "Открыть папку с кэшем шейдеров", "sv_SE": "Öppna katalog för shader cache", - "th_TH": "เปิดไดเรกทอรี่ แคช แสงเงา", + "th_TH": "เปิดไดเรกทอรีแคชเชเดอร์", "tr_TR": "Shader Önbelleği Dizinini Aç", "uk_UA": "Відкрити теку з кешем шейдерів", "zh_CN": "打开着色器缓存目录", @@ -2435,7 +2435,7 @@ "pt_BR": "Extrair Dados", "ru_RU": "Извлечь данные", "sv_SE": "Extrahera data", - "th_TH": "แยกส่วนข้อมูล", + "th_TH": "แยกข้อมูลออกมา", "tr_TR": "Veriyi Ayıkla", "uk_UA": "Видобути дані", "zh_CN": "提取数据", @@ -2485,7 +2485,7 @@ "pt_BR": "Extrai a seção ExeFS do jogo (incluindo atualizações)", "ru_RU": "Извлечение раздела ExeFS из текущих настроек приложения (включая обновления)", "sv_SE": "Extrahera ExeFS-sektionen från applikationens aktuella konfiguration (inkl uppdateringar)", - "th_TH": "แยกส่วน ExeFS ออกจากการตั้งค่าปัจจุบันของแอปพลิเคชัน (รวมถึงอัปเดต)", + "th_TH": "แยกข้อมูลส่วน ExeFS จากคอนฟิกปัจจุบันของแอป (รวมถึงอัปเดต)", "tr_TR": "Uygulamanın geçerli yapılandırmasından ExeFS kısmını ayıkla (Güncellemeler dahil)", "uk_UA": "Видобуває розділ ExeFS із поточної конфігурації програми (включаючи оновлення)", "zh_CN": "从游戏的当前状态中提取 ExeFS 分区 (包括更新)", @@ -2535,7 +2535,7 @@ "pt_BR": "Extrai a seção RomFS do jogo (incluindo atualizações)", "ru_RU": "Извлечение раздела RomFS из текущих настроек приложения (включая обновления)", "sv_SE": "Extrahera RomFS-sektionen från applikationens aktuella konfiguration (inkl uppdateringar)", - "th_TH": "แยกส่วน RomFS ออกจากการตั้งค่าปัจจุบันของแอปพลิเคชัน (รวมถึงอัพเดต)", + "th_TH": "แยกข้อมูลส่วน RomFS จากคอนฟิกปัจจุบันของแอป (รวมถึงอัปเดต)", "tr_TR": "Uygulamanın geçerli yapılandırmasından RomFS kısmını ayıkla (Güncellemeler dahil)", "uk_UA": "Видобуває розділ RomFS із поточної конфігурації застосунку (включно з оновленнями)", "zh_CN": "从游戏的当前状态中提取 RomFS 分区 (包括更新)", @@ -2585,7 +2585,7 @@ "pt_BR": "Extraia o RomFS de um arquivo DLC selecionado", "ru_RU": "Извлекает файлы RomFS из выбранного файла DLC", "sv_SE": "Extrahera RomFS från en vald DLC-fil", - "th_TH": "", + "th_TH": "แยกข้อมูล RomFS จากไฟล์ DLC ที่เลือก", "tr_TR": "", "uk_UA": "Витягти RomFS з обраного файлу DLC", "zh_CN": "从选定的 DLC 文件中解压 RomFS", @@ -2635,7 +2635,7 @@ "pt_BR": "Extrai a seção Logo do jogo (incluindo atualizações)", "ru_RU": "Извлечение раздела с логотипом из текущих настроек приложения (включая обновления)", "sv_SE": "Extrahera Logo-sektionen från applikationens aktuella konfiguration (inkl uppdateringar)", - "th_TH": "แยกส่วน โลโก้ ออกจากการตั้งค่าปัจจุบันของแอปพลิเคชัน (รวมถึงอัปเดต)", + "th_TH": "แยกข้อมูลส่วนโลโก้จากคอนฟิกปัจจุบันของแอป (รวมถึงอัปเดต)", "tr_TR": "Uygulamanın geçerli yapılandırmasından Logo kısmını ayıkla (Güncellemeler dahil)", "uk_UA": "Видобуває розділ логотипу з поточної конфігурації програми (включаючи оновлення)", "zh_CN": "从游戏的当前状态中提取图标 (包括更新)", @@ -2685,7 +2685,7 @@ "pt_BR": "Criar um atalho na área de trabalho que inicia o aplicativo selecionado", "ru_RU": "Создает ярлык на рабочем столе, с помощью которого можно запустить игру или приложение", "sv_SE": "Skapa en skrivbordsgenväg som startar vald applikation", - "th_TH": "สร้างทางลัดบนเดสก์ท็อปสำหรับใช้แอปพลิเคชันที่เลือก", + "th_TH": "สร้างทางลัดบนเดสก์ท็อปเพื่อเปิดแอปพลิเคชันที่เลือก", "tr_TR": "Seçilmiş uygulamayı çalıştıracak bir masaüstü kısayolu oluştur", "uk_UA": "Створити ярлик на робочому столі, який запускатиме вибраний застосунок (гру)", "zh_CN": "创建一个直接启动此游戏的桌面快捷方式", @@ -2710,7 +2710,7 @@ "pt_BR": "Criar Configuração Custumizada", "ru_RU": "Задать индивидуальные параметры", "sv_SE": "Skapa anpassad konfiguration", - "th_TH": "", + "th_TH": "สร้างการตั้งค่าที่กำหนดเอง", "tr_TR": "", "uk_UA": "Створити користувацьку конфігурацію", "zh_CN": "创建自定义设置", @@ -2735,7 +2735,7 @@ "pt_BR": "Editar Configuração Customizada", "ru_RU": "Изменить индивидуальные параметры", "sv_SE": "Redigera anpassad konfiguration", - "th_TH": "", + "th_TH": "แก้ไขการตั้งค่าที่กำหนดเอง", "tr_TR": "", "uk_UA": "Редагувати користувацьку конфігурацію", "zh_CN": "编辑自定义设置", @@ -2760,7 +2760,7 @@ "pt_BR": "Crie um atalho na pasta Aplicativos do macOS que abre o Aplicativo selecionado", "ru_RU": "Создает ярлык игры или приложения в папке Программы macOS", "sv_SE": "Skapa en genväg i macOS-programmapp som startar vald applikation", - "th_TH": "สร้างทางลัดในโฟลเดอร์ Applications ของ macOS สำหรับใช้แอปพลิเคชันที่เลือก", + "th_TH": "สร้างทางลัดในโฟลเดอร์ Applications ของ macOS เพื่อเปิดแอปพลิเคชันที่เลือก", "tr_TR": "", "uk_UA": "Створити ярлик у каталозі програм macOS, що запускатиме обраний застосунок (гру)", "zh_CN": "在 macOS 的应用程序目录中创建一个直接启动此游戏的快捷方式", @@ -2785,7 +2785,7 @@ "pt_BR": "Mostrar Dados de Compatibilidade", "ru_RU": "Показать записи о совместимости", "sv_SE": "Visa kompatibilitetspost", - "th_TH": "", + "th_TH": "แสดงรายการความเข้ากันได้", "tr_TR": "", "uk_UA": "Iнформація про сумісність", "zh_CN": "显示兼容性项目", @@ -2810,7 +2810,7 @@ "pt_BR": "Exibe o jogo selecionado na Lista de Compatibilidade, que normalmente pode ser acessada pelo menu Ajuda.", "ru_RU": "Отобразить выбранную игру в списке совместимости, доступ к которому вы обычно можете получить через меню Справки.", "sv_SE": "Visa valt spel i kompatibilitetslistan som du normalt sett kan komma åt via hjälpmenyn.", - "th_TH": "", + "th_TH": "แสดงเกมที่เลือกในรายการความเข้ากันได้ ซึ่งปกติคุณสามารถเข้าถึงได้ผ่านเมนูช่วยเหลือ", "tr_TR": "", "uk_UA": "Показати цю гру в Списку Сумісності. Список сумісності також можна зайти в меню Довідки.", "zh_CN": "在兼容性列表中显示选定的游戏,您通常可以通过帮助菜单访问。", @@ -2860,7 +2860,7 @@ "pt_BR": "Editar sua configuração independente existente para o jogo selecionado", "ru_RU": "Отредактировать существующие независимые параметры для выбранной игры.", "sv_SE": "Redigera din befintliga oberoende konfiguration för det valda spelet", - "th_TH": "", + "th_TH": "แสดงรายละเอียดเกม", "tr_TR": "", "uk_UA": "Відредагувати наявну індивідуальну конфігурацію для цієї гри.", "zh_CN": "编辑选定游戏的现存独立配置", @@ -2885,7 +2885,7 @@ "pt_BR": "Mostrar Informações do Jogo", "ru_RU": "Показать информацию об игре", "sv_SE": "Visa spelinformation", - "th_TH": "", + "th_TH": "แสดงสถิติและรายละเอียดของเกมที่เลือกอยู่ในขณะนี้", "tr_TR": "", "uk_UA": "Інформація про гру", "zh_CN": "显示游戏信息", @@ -2960,7 +2960,7 @@ "pt_BR": "Abre o diretório Atmosphere do cartão SD alternativo que contém os Mods do aplicativo. Útil para mods que são empacotados para hardware real.", "ru_RU": "Открывает папку Atmosphere на альтернативной SD-карте, которая содержит моды для приложений и игр. Полезно для модов, сделанных для реальной консоли.", "sv_SE": "Öppnar den alternativa Atmosphere-katalogen på SD-kort som innehåller applikationens Mods. Användbart för Mods som är paketerade för riktig hårdvara.", - "th_TH": "เปิดไดเร็กทอรี่ Atmosphere ของการ์ด SD สำรองซึ่งมี Mods ของแอปพลิเคชัน ซึ่งมีประโยชน์สำหรับ Mods ที่บรรจุมากับฮาร์ดแวร์จริง", + "th_TH": "เปิดโฟลเดอร์ Atmosphere บนการ์ด SD ทางเลือกที่เก็บม็อดของแอปพลิเคชัน ใช้สำหรับม็อดที่จัดเตรียมมาเพื่อฮาร์ดแวร์จริง", "tr_TR": "", "uk_UA": "Відкриває альтернативну теку SD-карти Atmosphere, що містить модифікації до застосунків або ігор. Корисно для модифікацій, зроблених для реального обладнання.", "zh_CN": "打开存放适用于大气层系统的游戏 MOD 的目录,对于为真实硬件打包的 MOD 非常有用", @@ -2985,7 +2985,7 @@ "pt_BR": "Verificar e Reduzir o Arquivo XCI", "ru_RU": "Проверить и обрезать XCI файл", "sv_SE": "Kontrollera och optimera XCI-fil", - "th_TH": "", + "th_TH": "ตรวจสอบและลดขนาดไฟล์ XCI", "tr_TR": "", "uk_UA": "Перевірка та нарізка XCI Файлу", "zh_CN": "检查并瘦身 XCI 文件", @@ -3010,7 +3010,7 @@ "pt_BR": "Verifique e reduza o arquivo XCI para economizar espaço em disco", "ru_RU": "Проверить и обрезать XCI файл для уменьшения его размера", "sv_SE": "Kontrollera och optimera XCI-fil för att spara diskutrymme", - "th_TH": "", + "th_TH": "ตรวจสอบและลดขนาดไฟล์ XCI เพื่อประหยัดเนื้อที่เก็บข้อมูล", "tr_TR": "", "uk_UA": "Перевірити та обрізати XCI Файл задля збереження місця на диску", "zh_CN": "检查并瘦身 XCI 文件以节约磁盘空间", @@ -3035,7 +3035,7 @@ "pt_BR": "{0}/{1} Jogos Carregados", "ru_RU": "{0}/{1} игр загружено", "sv_SE": "{0}/{1} spel inlästa", - "th_TH": "เกมส์โหลดแล้ว {0}/{1}", + "th_TH": "โหลดเกมแล้ว {0} จากทั้งหมด {1} เกม", "tr_TR": "{0}/{1} Oyun Yüklendi", "uk_UA": "{0}/{1} ігор завантажено", "zh_CN": "{0}/{1} 游戏加载完成", @@ -3060,7 +3060,7 @@ "pt_BR": "Versão do Firmware: {0}", "ru_RU": "Версия прошивки: {0}", "sv_SE": "Firmware-version: {0}", - "th_TH": "", + "th_TH": "เวอร์ชันเฟิร์มแวร์: {0}", "tr_TR": "", "uk_UA": "Версія прошивки: {0}", "zh_CN": "系统固件版本:{0}", @@ -3085,7 +3085,7 @@ "pt_BR": "Reduzindo o Arquivo XCI '{0}'", "ru_RU": "Обрезается XCI файл '{0}'", "sv_SE": "Optimerar XCI-filen '{0}'", - "th_TH": "", + "th_TH": "กำลังลดขนาดไฟล์ XCI '{0}'", "tr_TR": "", "uk_UA": "Обрізається XCI Файлів '{0}'", "zh_CN": "正在瘦身 XCI 文件 '{0}'", @@ -3110,7 +3110,7 @@ "pt_BR": "Detectado limite baixo para mapeamentos de memória", "ru_RU": "Обнаружен низкий лимит разметки памяти", "sv_SE": "Låg gräns för minnesmappningar upptäcktes", - "th_TH": "การตั้งค่าหน่วยความถึงขีดจำกัดต่ำสุดแล้ว", + "th_TH": "ตรวจพบขีดจำกัดต่ำสำหรับการแมปหน่วยความจำ", "tr_TR": "Bellek Haritaları İçin Düşük Limit Tespit Edildi ", "uk_UA": "Виявлено низьку межу для відображення памʼяті", "zh_CN": "检测到操作系统内存映射最大数量被设置的过低", @@ -3135,7 +3135,7 @@ "pt_BR": "Você gostaria de aumentar o valor de vm.max_map_count para {0}", "ru_RU": "Хотите увеличить значение vm.max_map_count до {0}", "sv_SE": "Vill du öka värdet för vm.max_map_count till {0}", - "th_TH": "คุณต้องเพิ่มค่า vm.max_map_count ไปยัง {0}", + "th_TH": "คุณต้องการเพิ่มค่าของ vm.max_map_count เป็น {0} หรือไม่?", "tr_TR": "vm.max_map_count değerini {0} sayısına yükseltmek ister misiniz", "uk_UA": "Бажаєте збільшити значення vm.max_map_count до {0}", "zh_CN": "你想要将操作系统 vm.max_map_count 的值增加到 {0} 吗", @@ -3160,7 +3160,7 @@ "pt_BR": "Alguns jogos podem tentar criar mais mapeamentos de memória do que o atualmente permitido. Ryujinx irá falhar assim que este limite for excedido.", "ru_RU": "Некоторые игры могут создавать большую разметку памяти, чем разрешено на данный момент по умолчанию. Ryujinx вылетит при превышении этого лимита.", "sv_SE": "Vissa spel kan försöka att skapa fler minnesmappningar än vad som tillåts. Ryujinx kommer att krascha så snart som denna gräns överstigs.", - "th_TH": "บางเกมอาจพยายามใช้งานหน่วยความจำมากกว่าที่ได้รับอนุญาตในปัจจุบัน Ryujinx จะปิดตัวลงเมื่อเกินขีดจำกัดนี้", + "th_TH": "บางเกมอาจพยายามสร้างการแมปหน่วยความจำมากกว่าที่ระบบอนุญาตในขณะนี้ หากเกินขีดจำกัดนี้ Ryujinx จะหยุดทำงานทันที", "tr_TR": "Bazı oyunlar şu an izin verilen bellek haritası limitinden daha fazlasını yaratmaya çalışabilir. Ryujinx bu limitin geçildiği takdirde kendini kapatıcaktır.", "uk_UA": "Деякі ігри можуть спробувати створити більше відображень памʼяті, ніж це дозволено зараз. Ryujinx закриється (крашнеться), щойно цей ліміт буде перевищено.", "zh_CN": "有些游戏可能会尝试创建超过当前系统允许的内存映射最大数量,若超过当前最大数量,Ryujinx 模拟器将会闪退。", @@ -3185,7 +3185,7 @@ "pt_BR": "Sim, até a próxima reinicialização", "ru_RU": "Да, до следующего перезапуска", "sv_SE": "Ja, tills nästa omstart", - "th_TH": "ใช่, จนกว่าจะรีสตาร์ทครั้งถัดไป", + "th_TH": "ใช่, จนกว่าจะรีสตาร์ทเครื่องครั้งถัดไป", "tr_TR": "Evet, bir sonraki yeniden başlatmaya kadar", "uk_UA": "Так, до наст. перезапуску", "zh_CN": "确定,临时保存(重启后失效)", @@ -3210,7 +3210,7 @@ "pt_BR": "Sim, permanentemente", "ru_RU": "Да, постоянно", "sv_SE": "Ja, permanent", - "th_TH": "ใช่, อย่างถาวร", + "th_TH": "ใช่, ตลอดไป", "tr_TR": "Evet, kalıcı olarak", "uk_UA": "Так, постійно", "zh_CN": "确定,永久保存", @@ -3235,7 +3235,7 @@ "pt_BR": "A quantidade máxima de mapeamentos de memória é menor que a recomendada.", "ru_RU": "Максимальная разметка памяти меньше, чем рекомендуется.", "sv_SE": "Maximal mängd minnesmappningar är lägre än rekommenderat.", - "th_TH": "จำนวนสูงสุดของการจัดการหน่วยความจำ ต่ำกว่าที่แนะนำ", + "th_TH": "ขีดจำกัดสูงสุดของการแมปหน่วยความจำ ต่ำกว่าค่าที่แนะนำ", "tr_TR": "İzin verilen maksimum bellek haritası değeri tavsiye edildiğinden daha düşük. ", "uk_UA": "Максимальний обсяг виділеної пам'яті менший за рекомендований.", "zh_CN": "内存映射的最大数量低于推荐值。", @@ -3260,7 +3260,7 @@ "pt_BR": "O valor atual de vm.max_map_count ({0}) é menor que {1}. Alguns jogos podem tentar criar mais mapeamentos de memória do que o permitido no momento. Ryujinx vai falhar assim que este limite for excedido.\n\nTalvez você queira aumentar o limite manualmente ou instalar pkexec, o que permite que Ryujinx ajude com isso.", "ru_RU": "Текущее значение vm.max_map_count ({0}) меньше, чем {1}. Некоторые игры могут попытаться создать большую разметку памяти, чем разрешено в данный момент. Ryujinx вылетит как только этот лимит будет превышен.\n\nВозможно, вам потребуется вручную увеличить лимит или установить pkexec, что позволит Ryujinx помочь справиться с превышением лимита.", "sv_SE": "Det aktuella värdet för vm.max_map_count ({0}) är lägre än {1}. Vissa spel kan försöka att skapa fler minnesmappningar än vad som tillåts. Ryujinx kommer att krascha så snart som denna gräns överstigs.\n\nDu kanske vill manuellt öka gränsen eller installera pkexec, vilket tillåter att Ryujinx hjälper till med det.", - "th_TH": "ค่าปัจจุบันของ vm.max_map_count ({0}) มีค่าต่ำกว่า {1} บางเกมอาจพยายามใช้หน่วยความจำมากกว่าที่ได้รับอนุญาตในปัจจุบัน Ryujinx จะปิดตัวลงเมื่อเกินขีดจำกัดนี้\n\nคุณอาจต้องการตั้งค่าเพิ่มขีดจำกัดด้วยตนเองหรือติดตั้ง pkexec ซึ่งอนุญาตให้ Ryujinx ช่วยเหลือคุณได้", + "th_TH": "ค่าปัจจุบันของ vm.max_map_count ({0}) ต่ำกว่า {1} บางเกมอาจพยายามสร้างการแมปหน่วยความจำมากกว่าที่ระบบอนุญาตไว้ในขณะนี้ หากเกินขีดจำกัดนี้ Ryujinx จะเกิดข้อผิดพลาดและปิดตัวเองทันที คุณอาจต้องเพิ่มค่าขีดจำกัดนี้ด้วยตนเอง หรือจะติดตั้งโปรแกรม pkexec เพื่อให้ Ryujinx ช่วยจัดการให้ก็ได้", "tr_TR": "Şu anki vm.max_map_count değeri {0}, bu {1} değerinden daha az. Bazı oyunlar şu an izin verilen bellek haritası limitinden daha fazlasını yaratmaya çalışabilir. Ryujinx bu limitin geçildiği takdirde kendini kapatıcaktır.\n\nManuel olarak bu limiti arttırmayı deneyebilir ya da pkexec'i yükleyebilirsiniz, bu da Ryujinx'in yardımcı olmasına izin verir.", "uk_UA": "Поточне значення vm.max_map_count ({0}) менше за {1}. Деякі ігри можуть спробувати створити більше відображень пам’яті, ніж дозволено наразі. Ryujinx закриється (крашнеться), щойно цей ліміт буде перевищено.\n\nВи можете збільшити ліміт власноруч або встановити pkexec, який допоможе Ryujinx впоратися з перевищенням ліміту.", "zh_CN": "vm.max_map_count ({0}) 的当前值小于 {1}。 有些游戏可能会尝试创建超过当前系统允许的内存映射最大数量,若超过当前最大数量,Ryujinx 模拟器将会闪退。\n\n你可以手动增加内存映射最大数量,或者安装 pkexec,它可以辅助 Ryujinx 完成内存映射最大数量的修改操作。", @@ -3335,7 +3335,7 @@ "pt_BR": "Interface do Usuário", "ru_RU": "Интерфейс", "sv_SE": "Användargränssnitt", - "th_TH": "หน้าจอผู้ใช้", + "th_TH": "อินเทอร์เฟซผู้ใช้", "tr_TR": "Kullancı Arayüzü", "uk_UA": "Інтерфейс", "zh_CN": "用户界面", @@ -3410,7 +3410,7 @@ "pt_BR": "Verificar Atualizações:", "ru_RU": "Проверка наличия обновлений", "sv_SE": "Leta efter uppdateringar:", - "th_TH": "", + "th_TH": "ตรวจสอบการอัปเดต:", "tr_TR": "", "uk_UA": "Перевірка оновлень:", "zh_CN": "检查更新", @@ -3435,7 +3435,7 @@ "pt_BR": "Desligado", "ru_RU": "Отключить", "sv_SE": "Av", - "th_TH": "", + "th_TH": "ปิด", "tr_TR": "", "uk_UA": "Вимкнути", "zh_CN": "关闭", @@ -3460,7 +3460,7 @@ "pt_BR": "Ao Abrir", "ru_RU": "При запуске", "sv_SE": "Fråga", - "th_TH": "", + "th_TH": "คำสั่ง", "tr_TR": "", "uk_UA": "Запитувати щоразу", "zh_CN": "提示", @@ -3485,7 +3485,7 @@ "pt_BR": "2° Plano", "ru_RU": "В фоне", "sv_SE": "Bakgrund", - "th_TH": "", + "th_TH": "พื้นหลัง", "tr_TR": "", "uk_UA": "Оновлювати в фоні", "zh_CN": "背景", @@ -3510,7 +3510,7 @@ "pt_BR": "Ao Perder o Foco:", "ru_RU": "При выходе эмулятора из фокуса", "sv_SE": "När emulatorn tappar fokus:", - "th_TH": "", + "th_TH": "เมื่อโปรแกรมจำลองไม่ถูกเลือก (สูญเสียโฟกัส):", "tr_TR": "", "uk_UA": "При втраті фокуса емулятором:", "zh_CN": "当模拟器在后台时:", @@ -3535,7 +3535,7 @@ "pt_BR": "Não Fazer Nada", "ru_RU": "Ничего не делать", "sv_SE": "Gör ingenting", - "th_TH": "", + "th_TH": "ไม่ตอบสนอง", "tr_TR": "", "uk_UA": "Нічого не робити", "zh_CN": "什么事情也不做", @@ -3560,7 +3560,7 @@ "pt_BR": "Bloquear Controles", "ru_RU": "Блокировать управление", "sv_SE": "Blockera inmatning", - "th_TH": "", + "th_TH": "ป้องกันการป้อนข้อมูล", "tr_TR": "", "uk_UA": "Блокувати введення", "zh_CN": "禁用输入", @@ -3585,7 +3585,7 @@ "pt_BR": "Ficar Mudo", "ru_RU": "Отключить звук", "sv_SE": "Stäng av ljudet", - "th_TH": "", + "th_TH": "ปิดเสียง", "tr_TR": "", "uk_UA": "Вимкнути звук", "zh_CN": "静音", @@ -3610,7 +3610,7 @@ "pt_BR": "Bloquear Controles & Ficar Mudo", "ru_RU": "Блокировать управление и отключить звук", "sv_SE": "Blockera inmatningar och stäng av ljudet", - "th_TH": "", + "th_TH": "ป้องกันการป้อนข้อมูลและปิดเสียง", "tr_TR": "", "uk_UA": "Блокувати введення та Вимкнути звук", "zh_CN": "阻止输入且静音", @@ -3635,7 +3635,7 @@ "pt_BR": "Pausar a Emulação", "ru_RU": "Поставить паузу", "sv_SE": "Pausa emuleringen", - "th_TH": "", + "th_TH": "พักการจำลอง", "tr_TR": "", "uk_UA": "Поставити на паузу", "zh_CN": "暂停模拟", @@ -3710,7 +3710,7 @@ "pt_BR": "Desativar Controles Quando Estiver Fora de Foco", "ru_RU": "Отключает управление при выходе из фокуса", "sv_SE": "Inaktivera inmatning när fokus tappas", - "th_TH": "", + "th_TH": "ปิดการพิมพ์เมื่อไม่ได้เลือกช่องอินพุต ไม่ให้กรอกข้อมูลเมื่อช่องอินพุตไม่อยู่ในโฟกัส", "tr_TR": "", "uk_UA": "Вимкнути введення, якщо вікно неактивне", "zh_CN": "在后台时禁用输入", @@ -3735,7 +3735,7 @@ "pt_BR": "Mostrar Estilo Original da Interface (Requer Reinicialização)", "ru_RU": "Включить оригинальный интерфейса (требуется перезагрузка)", "sv_SE": "Visa ursprunglig gränssnittsstil (kräver omstart)", - "th_TH": "", + "th_TH": "แสดงรูปแบบอินเทอร์เฟซเดิม (ต้องเริ่มโปรแกรมใหม่)", "tr_TR": "", "uk_UA": "Показати оригінальний UI (Потрібен перезапуск)", "zh_CN": "显示原始 UI 样式 (需要重启)", @@ -3760,7 +3760,7 @@ "pt_BR": "Mostrar a Interface Avalonia antiga do Ryujinx 1.1.1403. Esta versão é ativada por padrão nas plataformas que não sejam Windows.\n\nO estilo clássico da Barra de Título retorna e grande parte das mudanças do Layout de janela são revertidas; assim como as configurações de posicionamento da navegação acima dessa descrição.", "ru_RU": "Показать старый пользовательский интерфейс Avalonia Ryujinx, напоминающий Ryujinx 1.1.1403. Включено по умолчанию на платформах, отличных от Windows.\n\nСтрока заголовка в классическом стиле вернётся на место, а основные изменения в оформлении окна будут отменены; например, расположение навигации по настройкам над этой всплывающей подсказкой.", "sv_SE": "Visa det gamla Ryuijinx-gränssnittet baserat på Avalonia som påminner om version 1.1.1403. Detta är aktiverat som standard på plattformat som inte är Windows.\n\nDen klassiska titelfältet är tillbaka och de stora omarbetningarna av fönsterlayouten är omvända, till exempel placeringen av inställningsnavigeringen ovanför detta verktygstips.", - "th_TH": "", + "th_TH": "แสดง UI แบบเก่าของ Avalonia Ryujinx ที่คล้ายกับเวอร์ชัน 1.1.1403 (เปิดใช้งานโดยอัตโนมัติในระบบที่ไม่ใช่ Windows)\n\nแถบชื่อหน้าต่างแบบดั้งเดิมกลับมาแล้ว และการจัดวางหน้าต่างที่เคยเปลี่ยนไปก็ถูกปรับกลับ เช่น ตำแหน่งของเมนูการตั้งค่าเหนือข้อความนี้", "tr_TR": "", "uk_UA": "Показати старий інтерфейс Avalonia Ryujinx, який був у Ryujinx 1.1.1403. Ця опція активна за замовчуванням на всіх інших, окрім Windows платформах.\n\nПовернеться класична панель заголовка, а всі суттєві зміни інтерфейсу будуть скасовані, зокрема горизонтальне розміщення навігації в налаштуваннях.", "zh_CN": "显示旧的类似 Ryujinx 1.1.1403 的 Avalonia Ryujinx UI。在非 Windows 平台上默认启用此选项。\n\n经典样式的标题栏已回归并且恢复了对窗口布局的重大重构;例如在工具提示上方放置设置导航。", @@ -3935,7 +3935,7 @@ "pt_BR": "DLCs e Atualizações que se referem a arquivos ausentes serão desabilitados automaticamente", "ru_RU": "DLC и обновления, которые ссылаются на отсутствующие файлы, будут выгружаться автоматически", "sv_SE": "DLC och speluppdateringar som refererar till saknade filer kommer inte att läsas in automatiskt", - "th_TH": "", + "th_TH": "DLC และอัปเดตที่มีการอ้างอิงถึงไฟล์ที่ไม่มีอยู่ จะถูกปิดการใช้งานโดยอัตโนมัติ", "tr_TR": "", "uk_UA": "DLC та Оновлення, які посилаються на відсутні файли, будуть автоматично вимкнуті.", "zh_CN": "DLC 及 游戏更新 可自动加载和卸载", @@ -4835,7 +4835,7 @@ "pt_BR": "Multiplicador do Modo Turbo", "ru_RU": "", "sv_SE": "Multiplikator för turboläge:", - "th_TH": "", + "th_TH": "ระดับความแรงของโหมดเทอร์โบ:", "tr_TR": "", "uk_UA": "Множник Турборежиму:", "zh_CN": "涡轮模式倍数:", @@ -4860,7 +4860,7 @@ "pt_BR": "O valor do Multiplicador do Modo Turbo. Deixe em 200 se não tiver certeza.", "ru_RU": "", "sv_SE": "Målvärdet för multiplikatorn i turboläget. \n\nLämna den på 200 om du är osäker.", - "th_TH": "", + "th_TH": "กำหนดค่าตัวคูณสำหรับโหมดเทอร์โบ\n\n(หากไม่แน่ใจ ให้ปล่อยไว้ที่ค่าเริ่มต้น 200)", "tr_TR": "", "uk_UA": "Цільове значення коефіцієнта Турборежиму.\n\nЗалиште 200, якщо не впевнені", "zh_CN": "涡轮模式倍数的目标值。\n\n如果不确定请保留为 200。", @@ -4885,7 +4885,7 @@ "pt_BR": "O Modo Turbo é um recurso do emulador que efetivamente aumenta ou dimimui a velocidade de um jogo quando o mesmo não é sensivel à taxa de quadros. \nVocê pode ativar esse recurso dentro do jogo com uma tecla de atalho, configurável nas Configurações de Teclas de Atalho do Ryujinx. \n\nDeixe em 200 se não tiver certeza.", "ru_RU": "", "sv_SE": "Turboläget är en emulatorfunktion som effektivt ökar eller sänker hastigheten när ett spel inte är känsligt för bildfrekvens.\nDu kan växla denna funktion i spelet med en snabbtangent, konfigurerbar i Ryujinx inställningar för snabbtangenter.\n\nLämna den på 200 om du är osäker.", - "th_TH": "", + "th_TH": "โหมดเทอร์โบเป็นฟีเจอร์ของอีมูเลเตอร์ ที่ช่วยเร่งหรือชะลอความเร็วของเกม ในกรณีที่เกมไม่ไวต่ออัตราเฟรม\nคุณสามารถเปิดหรือปิดฟีเจอร์นี้ขณะเล่นเกมได้ด้วยปุ่มลัด ซึ่งสามารถตั้งค่าได้ในเมนู Ryujinx > การตั้งค่าปุ่มลัดบนคีย์บอร์ด\n\n(หากไม่แน่ใจ แนะนำให้ปล่อยค่าที่ 200)", "tr_TR": "", "uk_UA": "Турборежим (Turbo mode) – функція емулятора, що ефективно прискорює або сповільнює гру, якщо та не чутлива до частоти кадрів. Цю функцію можна ввімкнути/вимкнути безпосередньо під час гри за допомогою гарячої клавіші, яку можна прив'язати в меню \"Гарячі клавіші\" в налаштуваннях.", "zh_CN": "涡轮模式是一种模拟器功能当游戏对帧率不敏感时它可以有效地导致加速或降速。\n您可以在游戏中使用热键切换此功能,它可以在 Ryujinx 的键盘热键设置进行设置。\n\n如果不确定则保留为 200。", @@ -5135,7 +5135,7 @@ "pt_BR": "4GB", "ru_RU": "4ГиБ", "sv_SE": "", - "th_TH": "", + "th_TH": "4 กิบบิไบต์", "tr_TR": "", "uk_UA": "4Гб", "zh_CN": "", @@ -5160,7 +5160,7 @@ "pt_BR": "6GB", "ru_RU": "6ГиБ", "sv_SE": "", - "th_TH": "", + "th_TH": "6 กิบบิไบต์", "tr_TR": "", "uk_UA": "6Гб", "zh_CN": "", @@ -5185,7 +5185,7 @@ "pt_BR": "8GB", "ru_RU": "8ГиБ", "sv_SE": "", - "th_TH": "", + "th_TH": "8 กิบบิไบต์", "tr_TR": "", "uk_UA": "8Гб", "zh_CN": "", @@ -5210,7 +5210,7 @@ "pt_BR": "12GB", "ru_RU": "12ГиБ", "sv_SE": "", - "th_TH": "", + "th_TH": "12 กิบบิไบต์", "tr_TR": "", "uk_UA": "12Гб", "zh_CN": "", @@ -5435,7 +5435,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "คูณ 2", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -5460,7 +5460,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "คูณ 4", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -5485,7 +5485,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "คูณ 8", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -5510,7 +5510,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "คูณ 16", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -5610,7 +5610,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "คูณ 2 (1440p/2160p)", "tr_TR": "", "uk_UA": "", "zh_CN": "2 倍 (1440p/2160p)", @@ -5635,7 +5635,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "คูณ 3 (2160p/3240p)", "tr_TR": "", "uk_UA": "", "zh_CN": "3 倍 (2160p/3240p)", @@ -5660,7 +5660,7 @@ "pt_BR": "4x (2880p/4320p) (Não recomendado)", "ru_RU": "4x (2880p/4320p) (не рекомендуется)", "sv_SE": "4x (2880p/4320p) (rekommenderas inte)", - "th_TH": "4x (2880p/4320p) (ไม่แนะนำ)", + "th_TH": "คูณ 4 (2880p/4320p) (ไม่แนะนำ)", "tr_TR": "4x (2880p/4320p) (Tavsiye Edilmez)", "uk_UA": "4x (2880p/4320p) (Не рекомендується)", "zh_CN": "4 倍 (2880p/4320p) (不推荐)", @@ -8585,7 +8585,7 @@ "pt_BR": "", "ru_RU": "LED-подсветка", "sv_SE": "", - "th_TH": "", + "th_TH": "ตั้งค่าไฟ LED", "tr_TR": "", "uk_UA": "LED-підсвітка", "zh_CN": "", @@ -8610,7 +8610,7 @@ "pt_BR": "Desabilitar", "ru_RU": "Отключить", "sv_SE": "Inaktivera", - "th_TH": "", + "th_TH": "ปิดการใช้งาน", "tr_TR": "", "uk_UA": "Вимкнути", "zh_CN": "关闭", @@ -8635,7 +8635,7 @@ "pt_BR": "Arco-íris", "ru_RU": "Радужная", "sv_SE": "Regnbåge", - "th_TH": "", + "th_TH": "สีรุ้ง", "tr_TR": "", "uk_UA": "Веселка", "zh_CN": "彩虹", @@ -8660,7 +8660,7 @@ "pt_BR": "Velocidade do Arco-íris", "ru_RU": "Скорость переливания", "sv_SE": "Regnbågshastighet", - "th_TH": "", + "th_TH": "ความเร็วของสีรุ้ง", "tr_TR": "", "uk_UA": "Швидкість зміни кольорів", "zh_CN": "彩虹滚动速度", @@ -8685,7 +8685,7 @@ "pt_BR": "Cor", "ru_RU": "Цвет", "sv_SE": "Färg", - "th_TH": "", + "th_TH": "สี", "tr_TR": "", "uk_UA": "Колір", "zh_CN": "颜色", @@ -8785,7 +8785,7 @@ "pt_BR": "Shift Esquerdo", "ru_RU": "Левый Shift", "sv_SE": "Skift vänster", - "th_TH": "", + "th_TH": "Shift ซ้าย", "tr_TR": "Sol Shift", "uk_UA": "Shift Лівий", "zh_CN": "左侧Shift", @@ -8810,7 +8810,7 @@ "pt_BR": "Shift Direito", "ru_RU": "Правый Shift", "sv_SE": "Skift höger", - "th_TH": "", + "th_TH": "Shift ขวา", "tr_TR": "Sağ Shift", "uk_UA": "Shift Правий", "zh_CN": "右侧Shift", @@ -8835,7 +8835,7 @@ "pt_BR": "Ctrl Esquerdo", "ru_RU": "Левый Ctrl", "sv_SE": "Ctrl vänster", - "th_TH": "", + "th_TH": "Ctrl ซ้าย", "tr_TR": "Sol Ctrl", "uk_UA": "Ctrl Лівий", "zh_CN": "左侧Ctrl", @@ -8860,7 +8860,7 @@ "pt_BR": "⌃ Esquerda", "ru_RU": "Левый ⌃", "sv_SE": "^ Vänster", - "th_TH": "", + "th_TH": "^ ซ้าย", "tr_TR": "⌃ Sol", "uk_UA": "⌃ Лівий", "zh_CN": "左侧⌃", @@ -8885,7 +8885,7 @@ "pt_BR": "Ctrl Direito", "ru_RU": "Правый Ctrl", "sv_SE": "Ctrl höger", - "th_TH": "", + "th_TH": "Ctrl ขวา", "tr_TR": "Sağ Control", "uk_UA": "Ctrl Правий", "zh_CN": "右侧Ctrl", @@ -8910,7 +8910,7 @@ "pt_BR": "⌃ Direito", "ru_RU": "Правый ⌃", "sv_SE": "^ Höger", - "th_TH": "", + "th_TH": "⌃ ขวา", "tr_TR": "⌃ Sağ", "uk_UA": "⌃ Правий", "zh_CN": "右侧⌃", @@ -8935,7 +8935,7 @@ "pt_BR": "Alt Esquerdo", "ru_RU": "Левый Alt", "sv_SE": "Alt vänster", - "th_TH": "", + "th_TH": "Alt ซ้าย", "tr_TR": "Sol Alt", "uk_UA": "Alt Лівий", "zh_CN": "左侧Alt", @@ -8960,7 +8960,7 @@ "pt_BR": "⌥ Esquerda", "ru_RU": "Левый ⌥", "sv_SE": "⌥ vänster", - "th_TH": "", + "th_TH": "⌥ ซ้าย", "tr_TR": "⌥ Sol", "uk_UA": "⌥ Лівий", "zh_CN": "左侧⌥", @@ -8985,7 +8985,7 @@ "pt_BR": "Alt Direito", "ru_RU": "Правый Alt", "sv_SE": "Alt höger", - "th_TH": "", + "th_TH": "Alt ขวา", "tr_TR": "Sağ Alt", "uk_UA": "Alt Правий", "zh_CN": "右侧Alt", @@ -9010,7 +9010,7 @@ "pt_BR": "⌥ Direito", "ru_RU": "Правый ⌥", "sv_SE": "⌥ höger", - "th_TH": "", + "th_TH": "⌥ ขวา", "tr_TR": "⌥ Sağ", "uk_UA": "⌥ Правий", "zh_CN": "右侧⌥", @@ -9035,7 +9035,7 @@ "pt_BR": "⊞ Esquerdo", "ru_RU": "Левый ⊞", "sv_SE": "⊞ vänster", - "th_TH": "", + "th_TH": "⊞ ซ้าย", "tr_TR": "⊞ Sol", "uk_UA": "⊞ Лівий", "zh_CN": "左侧⊞", @@ -9060,7 +9060,7 @@ "pt_BR": "⌘ Esquerdo", "ru_RU": "Левый ⌘", "sv_SE": "⌘ vänster", - "th_TH": "", + "th_TH": "⌘ ซ้าย", "tr_TR": "⌘ Sol", "uk_UA": "⌘ Лівий", "zh_CN": "左侧⌘", @@ -9085,7 +9085,7 @@ "pt_BR": "⊞ Direito", "ru_RU": "Правый ⊞", "sv_SE": "⊞ höger", - "th_TH": "", + "th_TH": "⊞ ขวา", "tr_TR": "⊞ Sağ", "uk_UA": "⊞ Правий", "zh_CN": "右侧⊞", @@ -9110,7 +9110,7 @@ "pt_BR": "⌘ Direito", "ru_RU": "Правый ⌘", "sv_SE": "⌘ höger", - "th_TH": "", + "th_TH": "⌘ ขวา", "tr_TR": "⌘ Sağ", "uk_UA": "⌘ Правий", "zh_CN": "右侧⌘", @@ -9135,7 +9135,7 @@ "pt_BR": "", "ru_RU": "Меню", "sv_SE": "Meny", - "th_TH": "", + "th_TH": "เมนู", "tr_TR": "Menü", "uk_UA": "Меню", "zh_CN": "菜单键", @@ -9160,7 +9160,7 @@ "pt_BR": "Cima", "ru_RU": "Вверх", "sv_SE": "Upp", - "th_TH": "", + "th_TH": "ขึ้น", "tr_TR": "Yukarı", "uk_UA": "Вгору ↑", "zh_CN": "上", @@ -9185,7 +9185,7 @@ "pt_BR": "Baixo", "ru_RU": "Вниз", "sv_SE": "Ner", - "th_TH": "", + "th_TH": "ลง", "tr_TR": "Aşağı", "uk_UA": "Вниз ↓", "zh_CN": "下", @@ -9210,7 +9210,7 @@ "pt_BR": "Esquerda", "ru_RU": "Влево", "sv_SE": "Vänster", - "th_TH": "", + "th_TH": "ซ้าย", "tr_TR": "Sol", "uk_UA": "Вліво ←", "zh_CN": "左", @@ -9235,7 +9235,7 @@ "pt_BR": "Direita", "ru_RU": "Вправо", "sv_SE": "Höger", - "th_TH": "", + "th_TH": "ขวา", "tr_TR": "Sağ", "uk_UA": "Вправо →", "zh_CN": "右", @@ -9260,7 +9260,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Enter", "tr_TR": "", "uk_UA": "", "zh_CN": "回车键", @@ -9285,7 +9285,7 @@ "pt_BR": "Esc", "ru_RU": "Esc", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Escape", "tr_TR": "Esc", "uk_UA": "Esc", "zh_CN": "Esc", @@ -9310,7 +9310,7 @@ "pt_BR": "Espaço", "ru_RU": "Пробел", "sv_SE": "Blanksteg", - "th_TH": "", + "th_TH": "ปุ่ม Spacebar", "tr_TR": "", "uk_UA": "Пробіл", "zh_CN": "空格键", @@ -9335,7 +9335,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Tab", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -9360,7 +9360,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Backspace", "tr_TR": "Geri tuşu", "uk_UA": "", "zh_CN": "退格键", @@ -9385,7 +9385,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Insert", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -9410,7 +9410,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Delete", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -9435,7 +9435,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Page Up", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -9460,7 +9460,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Page Down", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -9485,7 +9485,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Home", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -9510,7 +9510,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม End", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -9535,7 +9535,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Caps Lock", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -9560,7 +9560,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Scroll Lock", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -9585,7 +9585,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Print Screen", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -9610,7 +9610,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Pause", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -9635,7 +9635,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม Num Lock", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -9660,7 +9660,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "Töm", - "th_TH": "", + "th_TH": "ล้าง", "tr_TR": "", "uk_UA": "Очистити", "zh_CN": "清除键", @@ -9685,7 +9685,7 @@ "pt_BR": "", "ru_RU": "0 (цифровий блок)", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม 0 บนแป้นตัวเลข", "tr_TR": "", "uk_UA": "Блок цифр 0", "zh_CN": "小键盘0", @@ -9710,7 +9710,7 @@ "pt_BR": "", "ru_RU": "Блок цифр 1", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม 1 บนแป้นตัวเลข", "tr_TR": "", "uk_UA": "Блок цифр 1", "zh_CN": "小键盘1", @@ -9735,7 +9735,7 @@ "pt_BR": "", "ru_RU": "Блок цифр 2", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม 2 บนแป้นตัวเลข", "tr_TR": "", "uk_UA": "2 (цифровий блок)", "zh_CN": "小键盘2", @@ -9760,7 +9760,7 @@ "pt_BR": "", "ru_RU": "Блок цифр 3", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม 3 บนแป้นตัวเลข", "tr_TR": "", "uk_UA": "3 (цифровий блок)", "zh_CN": "小键盘3", @@ -9785,7 +9785,7 @@ "pt_BR": "", "ru_RU": "Блок цифр 4", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม 4 บนแป้นตัวเลข", "tr_TR": "", "uk_UA": "4 (цифровий блок)", "zh_CN": "小键盘4", @@ -9810,7 +9810,7 @@ "pt_BR": "", "ru_RU": "Блок цифр 5", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม 5 บนแป้นตัวเลข", "tr_TR": "", "uk_UA": "5 (цифровий блок)", "zh_CN": "小键盘5", @@ -9835,7 +9835,7 @@ "pt_BR": "", "ru_RU": "Блок цифр 6", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม 6 บนแป้นตัวเลข", "tr_TR": "", "uk_UA": "6 (цифровий блок)", "zh_CN": "小键盘6", @@ -9860,7 +9860,7 @@ "pt_BR": "", "ru_RU": "Блок цифр 7", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม 7 บนแป้นตัวเลข", "tr_TR": "", "uk_UA": "7 (цифровий блок)", "zh_CN": "小键盘7", @@ -9885,7 +9885,7 @@ "pt_BR": "", "ru_RU": "Блок цифр 8", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม 8 บนแป้นตัวเลข", "tr_TR": "", "uk_UA": "8 (цифровий блок)", "zh_CN": "小键盘8", @@ -9910,7 +9910,7 @@ "pt_BR": "", "ru_RU": "Блок цифр 9", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม 9 บนแป้นตัวเลข", "tr_TR": "", "uk_UA": "9 (цифровий блок)", "zh_CN": "小键盘9", @@ -9935,7 +9935,7 @@ "pt_BR": "", "ru_RU": "/ (блок цифр)", "sv_SE": "Keypad /", - "th_TH": "", + "th_TH": "ปุ่ม / บนแป้นตัวเลข", "tr_TR": "", "uk_UA": "/ (цифровий блок)", "zh_CN": "小键盘/", @@ -9960,7 +9960,7 @@ "pt_BR": "", "ru_RU": "* (блок цифр)", "sv_SE": "Keypad *", - "th_TH": "", + "th_TH": "ปุ่ม * บนแป้นตัวเลข", "tr_TR": "", "uk_UA": "* (цифровий блок)", "zh_CN": "小键盘*", @@ -9985,7 +9985,7 @@ "pt_BR": "", "ru_RU": "- (блок цифр)", "sv_SE": "Keypad -", - "th_TH": "", + "th_TH": "ปุ่ม - บนแป้นตัวเลข", "tr_TR": "", "uk_UA": "- (цифровий блок)", "zh_CN": "小键盘-", @@ -10010,7 +10010,7 @@ "pt_BR": "", "ru_RU": "+ (блок цифр)", "sv_SE": "Keypad +", - "th_TH": "", + "th_TH": "ปุ่ม + บนแป้นตัวเลข", "tr_TR": "", "uk_UA": "+ (цифровий блок)", "zh_CN": "小键盘+", @@ -10035,7 +10035,7 @@ "pt_BR": "", "ru_RU": ". (блок цифр)", "sv_SE": "Keypad ,", - "th_TH": "", + "th_TH": "ปุ่ม . บนแป้นตัวเลข", "tr_TR": "", "uk_UA": ". (цифровий блок)", "zh_CN": "小键盘.", @@ -10060,7 +10060,7 @@ "pt_BR": "", "ru_RU": "Enter (блок цифр)", "sv_SE": "Enter (numerisk)", - "th_TH": "", + "th_TH": "ปุ่ม Enter บนแป้นตัวเลข", "tr_TR": "", "uk_UA": "Enter (цифровий блок)", "zh_CN": "小键盘回车键", @@ -10635,7 +10635,7 @@ "pt_BR": "Não Atribuído", "ru_RU": "Не привязано", "sv_SE": "Obunden", - "th_TH": "", + "th_TH": "ยังไม่กำหนดปุ่ม", "tr_TR": "", "uk_UA": "Відв'язати", "zh_CN": "未分配", @@ -10660,7 +10660,7 @@ "pt_BR": "Botão Analógico Esquerdo", "ru_RU": "Кнопка лев. стика", "sv_SE": "L-spakknapp", - "th_TH": "", + "th_TH": "ปุ่มกดที่แท่งอนาล็อกซ้าย", "tr_TR": "", "uk_UA": "L Кнопка Стіку", "zh_CN": "左摇杆按键", @@ -10685,7 +10685,7 @@ "pt_BR": "Botão Analógico Direito", "ru_RU": "Кнопка пр. стика", "sv_SE": "R-spakknapp", - "th_TH": "", + "th_TH": "ปุ่มกดที่แท่งอนาล็อกขวา", "tr_TR": "", "uk_UA": "R Кнопка Стіку", "zh_CN": "右摇杆按键", @@ -10710,7 +10710,7 @@ "pt_BR": "Ombro Esquerdo", "ru_RU": "Левый бампер", "sv_SE": "Vänster kantknapp", - "th_TH": "", + "th_TH": "ปุ่ม L (ไหล่ซ้าย)", "tr_TR": "", "uk_UA": "Лівий Бампер", "zh_CN": "左肩键L", @@ -10735,7 +10735,7 @@ "pt_BR": "Ombro Direito", "ru_RU": "Правый бампер", "sv_SE": "Höger kantknapp", - "th_TH": "", + "th_TH": "ปุ่ม R (ไหล่ขวา)", "tr_TR": "", "uk_UA": "Правий Бампер", "zh_CN": "右肩键R", @@ -10760,7 +10760,7 @@ "pt_BR": "Gatilho Esquerdo", "ru_RU": "Левый триггер", "sv_SE": "Vänster avtryckare", - "th_TH": "", + "th_TH": "ปุ่มทริกเกอร์ซ้าย", "tr_TR": "", "uk_UA": "Лівий Тригер", "zh_CN": "左扳机键ZL", @@ -10785,7 +10785,7 @@ "pt_BR": "Gatilho Direito", "ru_RU": "Правый триггер", "sv_SE": "Höger avtryckare", - "th_TH": "", + "th_TH": "ปุ่มทริกเกอร์ขวา", "tr_TR": "", "uk_UA": "Правий Тригер", "zh_CN": "右扳机键ZR", @@ -10810,7 +10810,7 @@ "pt_BR": "Cima", "ru_RU": "Вверх", "sv_SE": "Upp", - "th_TH": "", + "th_TH": "ขึ้น", "tr_TR": "", "uk_UA": "Вверх", "zh_CN": "上键", @@ -10835,7 +10835,7 @@ "pt_BR": "Baixo", "ru_RU": "Вниз", "sv_SE": "Ner", - "th_TH": "", + "th_TH": "ลง", "tr_TR": "", "uk_UA": "Вниз", "zh_CN": "下键", @@ -10860,7 +10860,7 @@ "pt_BR": "Esquerda", "ru_RU": "Влево", "sv_SE": "Vänster", - "th_TH": "", + "th_TH": "ซ้าย", "tr_TR": "", "uk_UA": "Вліво", "zh_CN": "左键", @@ -10885,7 +10885,7 @@ "pt_BR": "Direita", "ru_RU": "Вправо", "sv_SE": "Höger", - "th_TH": "", + "th_TH": "ขวา", "tr_TR": "Sağ", "uk_UA": "Вправо", "zh_CN": "右键", @@ -10910,7 +10910,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม - บนจอยเกม", "tr_TR": "", "uk_UA": "", "zh_CN": "-键", @@ -10935,7 +10935,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม + บนจอยเกม", "tr_TR": "4", "uk_UA": "", "zh_CN": "+键", @@ -10960,7 +10960,7 @@ "pt_BR": "Guia", "ru_RU": "Кнопка меню", "sv_SE": "", - "th_TH": "", + "th_TH": "คู่มือ", "tr_TR": "Rehber", "uk_UA": "Меню", "zh_CN": "主页键", @@ -10985,7 +10985,7 @@ "pt_BR": "", "ru_RU": "Прочее", "sv_SE": "Diverse", - "th_TH": "", + "th_TH": "เบ็ดเตล็ด", "tr_TR": "Diğer", "uk_UA": "Інше", "zh_CN": "截图键", @@ -11010,7 +11010,7 @@ "pt_BR": "", "ru_RU": "Доп.кнопка 1", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม พาเดิล 1", "tr_TR": "Pedal 1", "uk_UA": "Додаткова кнопка 1", "zh_CN": "其他按键1", @@ -11035,7 +11035,7 @@ "pt_BR": "", "ru_RU": "Доп.кнопка 2", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม พาเดิล 2", "tr_TR": "Pedal 2", "uk_UA": "Додаткова кнопка 2", "zh_CN": "其他按键2", @@ -11060,7 +11060,7 @@ "pt_BR": "", "ru_RU": "Доп.кнопка 3", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม พาเดิล 3", "tr_TR": "Pedal 3", "uk_UA": "Додаткова кнопка 3", "zh_CN": "其他按键3", @@ -11085,7 +11085,7 @@ "pt_BR": "", "ru_RU": "Доп.кнопка 4", "sv_SE": "", - "th_TH": "", + "th_TH": "ปุ่ม พาเดิล 4", "tr_TR": "Pedal 4", "uk_UA": "Додаткова кнопка 4", "zh_CN": "其他按键4", @@ -11110,7 +11110,7 @@ "pt_BR": "", "ru_RU": "Тачпад", "sv_SE": "Pekplatta", - "th_TH": "", + "th_TH": "ทัชแพด", "tr_TR": "", "uk_UA": "Сенсорна панель", "zh_CN": "触摸板", @@ -11135,7 +11135,7 @@ "pt_BR": "Gatilho Esquerdo 0", "ru_RU": "Левый триггер 0", "sv_SE": "Vänster avtryckare 0", - "th_TH": "", + "th_TH": "ปุ่ม 0 ทริกเกอร์ซ้าย", "tr_TR": "Sol Tetik 0", "uk_UA": "Лівий Тригер 0", "zh_CN": "左扳机0", @@ -11160,7 +11160,7 @@ "pt_BR": "Gatilho Direito 0", "ru_RU": "Правый триггер 0", "sv_SE": "Höger avtryckare 0", - "th_TH": "", + "th_TH": "ปุ่ม 0 ทริกเกอร์ขวา", "tr_TR": "Sağ Tetik 0", "uk_UA": "Правий Тригер 0", "zh_CN": "右扳机0", @@ -11185,7 +11185,7 @@ "pt_BR": "Gatilho Esquerdo 1", "ru_RU": "Левый триггер 1", "sv_SE": "Vänster avtryckare 1", - "th_TH": "", + "th_TH": "ปุ่ม 1 ทริกเกอร์ซ้าย", "tr_TR": "Sol Tetik 1", "uk_UA": "Лівий Тригер 1", "zh_CN": "左扳机1", @@ -11210,7 +11210,7 @@ "pt_BR": "Gatilho Direito 1", "ru_RU": "Правый триггер 1", "sv_SE": "Höger avtryckare 1", - "th_TH": "", + "th_TH": "ปุ่ม 1 ทริกเกอร์ขวา", "tr_TR": "Sağ Tetik 1", "uk_UA": "Правий Тригер 1", "zh_CN": "右扳机1", @@ -11235,7 +11235,7 @@ "pt_BR": "Analógico Esquerdo", "ru_RU": "Левый стик", "sv_SE": "Vänster spak", - "th_TH": "", + "th_TH": "แท่งอนาล็อกซ้าย", "tr_TR": "Sol Çubuk", "uk_UA": "Лівий Стік", "zh_CN": "左摇杆", @@ -11260,7 +11260,7 @@ "pt_BR": "Analógico Direito", "ru_RU": "Правый стик", "sv_SE": "Höger spak", - "th_TH": "", + "th_TH": "แท่งอนาล็อกขวา", "tr_TR": "Sağ çubuk", "uk_UA": "Правий Стік", "zh_CN": "右摇杆", @@ -11685,7 +11685,7 @@ "pt_BR": "Cancelando", "ru_RU": "Отмена", "sv_SE": "Avbryter", - "th_TH": "", + "th_TH": "กำลังทำการยกเลิก", "tr_TR": "", "uk_UA": "Скасування", "zh_CN": "正在取消", @@ -11710,7 +11710,7 @@ "pt_BR": "Fechar", "ru_RU": "Закрыть", "sv_SE": "Stäng", - "th_TH": "", + "th_TH": "ปิด", "tr_TR": "", "uk_UA": "Закрити", "zh_CN": "关闭", @@ -11910,7 +11910,7 @@ "pt_BR": "Ver Perfil", "ru_RU": "Показать профиль", "sv_SE": "Visa profil", - "th_TH": "", + "th_TH": "ดูโปรไฟล์", "tr_TR": "", "uk_UA": "Показати профіль", "zh_CN": "预览配置文件", @@ -12860,7 +12860,7 @@ "pt_BR": "Falha em atualizar a versão do Ryujinx recebida do servidor de atualização.", "ru_RU": "", "sv_SE": "Det gick inte att konvertera Ryujinx-versionen som mottogs från uppdateringsservern.", - "th_TH": "", + "th_TH": "ไม่สามารถแปลงเวอร์ชัน Ryujinx ที่ได้รับจากเซิร์ฟเวอร์อัปเดตได้", "tr_TR": "", "uk_UA": "Не вдалося конвертувати версію Ryujinx, отриману з сервера оновлень.", "zh_CN": "无法转换从更新服务器接收的 Ryujinx 版本。", @@ -13010,7 +13010,7 @@ "pt_BR": "Ver Registro de Alterações", "ru_RU": "Показать список изменений", "sv_SE": "Visa ändringslogg", - "th_TH": "", + "th_TH": "แสดงบันทึกการเปลี่ยนแปลง", "tr_TR": "", "uk_UA": "Показати список змін", "zh_CN": "显示更新日志", @@ -13485,7 +13485,7 @@ "pt_BR": "Janela de Redução XCI", "ru_RU": "Окно триммера XCI", "sv_SE": "XCI-optimerare", - "th_TH": "", + "th_TH": "หน้าต่างตัดแต่งไฟล์ XCI", "tr_TR": "", "uk_UA": "Вікно XCI Тримера", "zh_CN": "XCI 文件瘦身窗口", @@ -13685,7 +13685,7 @@ "pt_BR": "API Amiibo", "ru_RU": "API Amiibo", "sv_SE": "Amiibo-API", - "th_TH": "", + "th_TH": "อามิโบ้ API", "tr_TR": "", "uk_UA": "API Amiibo", "zh_CN": "", @@ -13935,7 +13935,7 @@ "pt_BR": "Você está prestes a limpar todos os dados PPTC de:\n\n{0}\n\nTem certeza de que deseja continuar?", "ru_RU": "Вы собираетесь удалить все данные PPTC из:\n\n{0}\n\nВы уверены, что хотите продолжить?", "sv_SE": "Du är på väg att ta bort allt PPTC-data från:\n\n{0}\n\nÄr du säker på att du vill fortsätta?", - "th_TH": "", + "th_TH": "คุณกำลังจะล้างข้อมูล PPTC ทั้งหมดจาก:\n\n{0}\n\nคุณแน่ใจหรือไม่ว่าต้องการดำเนินการต่อ?", "tr_TR": "", "uk_UA": "Ви збираєтесь видалити всі дані PPTC з:\n\n{0}\n\nБажаєте продовжити цю операцію?", "zh_CN": "您正要清理 PPTC 数据:\n\n{0}\n\n您确实要继续吗?", @@ -14235,7 +14235,7 @@ "pt_BR": "Um arquivo Chaves inválido foi encontrado em {0}", "ru_RU": "В {0} были найдены некорректные ключи", "sv_SE": "En ogiltig nyckelfil hittades i {0}", - "th_TH": "", + "th_TH": "พบไฟล์ Keys ที่ไม่ถูกต้องใน {0}", "tr_TR": "", "uk_UA": "Виявлено неправильний файл ключів у теці {0}", "zh_CN": "在 {0} 发现了一个无效的密匙文件", @@ -14260,7 +14260,7 @@ "pt_BR": "Instalar Chaves", "ru_RU": "Установить ключи", "sv_SE": "Installera nycklar", - "th_TH": "", + "th_TH": "ติดตั้งไฟล์ Keys", "tr_TR": "", "uk_UA": "Встановлення Ключів", "zh_CN": "安装密匙", @@ -14285,7 +14285,7 @@ "pt_BR": "O novo arquivo Chaves será instalado", "ru_RU": "Будут установлены новые ключи.", "sv_SE": "Ny nyckelfil kommer att installeras.", - "th_TH": "", + "th_TH": "กำลังติดตั้งไฟล์ Keys ใหม่", "tr_TR": "", "uk_UA": "Новий файл Ключів буде встановлено", "zh_CN": "将会安装新密匙文件", @@ -14310,7 +14310,7 @@ "pt_BR": "\n\nIsso pode substituir algumas das chaves instaladas atualmente.", "ru_RU": "\n\nЭто действие может перезаписать установленные ключи.", "sv_SE": "\n\nDetta kan ersätta några av de redan installerade nycklarna.", - "th_TH": "", + "th_TH": "\n\nสิ่งนี้อาจทำให้ไฟล์ Keys บางส่วนที่ติดตั้งอยู่ถูกแทนที่", "tr_TR": "", "uk_UA": "\n\nЦе замінить собою поточні файли Ключів.", "zh_CN": "\n\n这也许会替换掉一些当前已安装的密匙", @@ -14335,7 +14335,7 @@ "pt_BR": "\n\nVocê quer continuar?", "ru_RU": "\n\nХотите продолжить?", "sv_SE": "\n\nVill du fortsätta?", - "th_TH": "", + "th_TH": "\n\nคุณต้องการดำเนินการต่อหรือไม่?", "tr_TR": "", "uk_UA": "\n\nВи хочете продовжити?", "zh_CN": "\n\n你想要继续吗?", @@ -14360,7 +14360,7 @@ "pt_BR": "Instalando Chaves...", "ru_RU": "Установка ключей...", "sv_SE": "Installerar nycklar...", - "th_TH": "", + "th_TH": "กำลังดำเนินการติดตั้ง Keys...", "tr_TR": "", "uk_UA": "Встановлення Ключів...", "zh_CN": "安装密匙中。。。", @@ -14385,7 +14385,7 @@ "pt_BR": "Novo arquivo de chaves instalado com sucesso.", "ru_RU": "Новые ключи были успешно установлены.", "sv_SE": "Ny nyckelfil installerades.", - "th_TH": "", + "th_TH": "การติดตั้งไฟล์ Keys ใหม่เสร็จสมบูรณ์แล้ว", "tr_TR": "", "uk_UA": "Нові ключі встановлено.", "zh_CN": "已成功安装新密匙文件", @@ -15385,7 +15385,7 @@ "pt_BR": "Ryujinx é um emulador de Nintendo Switch™ 1.\nReceba todas as últimas notícias em nosso Discord.\nDesenvolvedores interessados em contribuir podem descobrir mais em nosso GitLab ou Discord.", "ru_RU": "Ryujinx - это эмулятор для Nintendo Switch™ 1.\nПолучайте все последние новости разработки в нашем Discord.\nРазработчики, заинтересованные в участии, могут узнать больше на нашем GitLab или Discord.", "sv_SE": "Ryujinx är en emulator för Nintendo Switch™ 1.\nFå de senaste nyheterna via vår Discord.\nUtvecklare som är intresserade att bidra kan hitta mer info på vår GitLab eller Discord.", - "th_TH": "", + "th_TH": "Ryujinx เป็นโปรแกรมจำลองสำหรับเครื่อง Nintendo Switch™ 1\nติดตามข่าวสารล่าสุดได้ที่ Discord ของเรา\nนักพัฒนาที่สนใจร่วมพัฒนา สามารถดูข้อมูลเพิ่มเติมได้ทาง GitLab หรือ Discord", "tr_TR": "", "uk_UA": "Ryujinx — це емулятор для Nintendo Switch™ 1.\nОстанні новини можна отримати в нашому Discord.\nРозробники, що бажають долучитись до розробки та зробити свій внесок, можуть отримати більше інформації на нашому GitLab або в Discord.", "zh_CN": "Ryujinx 是一个 Nintendo Switch™ 1 模拟器。\n有兴趣做出贡献的开发者可以在我们的 GitLab 或 Discord 上了解更多信息。\n", @@ -15435,7 +15435,7 @@ "pt_BR": "Anteriormente mantido por:", "ru_RU": "Поддержка:", "sv_SE": "Underhölls tidigare av:", - "th_TH": "", + "th_TH": "เคยได้รับการดูแลโดย:", "tr_TR": "", "uk_UA": "Минулі розробники:", "zh_CN": "曾经的维护者:", @@ -15485,7 +15485,7 @@ "pt_BR": "Franquia Amiibo", "ru_RU": "Серия Amiibo", "sv_SE": "Amiibo-serie", - "th_TH": "", + "th_TH": "ชุดซีรีส์ Amiibo", "tr_TR": "Amiibo Serisi", "uk_UA": "Серія Amiibo", "zh_CN": "Amiibo 系列", @@ -16285,7 +16285,7 @@ "pt_BR": "Se esta opção está ativada nas configurações customizadas, as configurações globais de entrada serão usadas.\n\nNas configurações globais: você pode ativar ou desativá-las se necessário; está configuração será herdada por qualquer nova configuração customizada criada.", "ru_RU": "Если эта опция включена в пользовательских настройках, будет использована глобальная конфигурация ввода.\n\nВ глобальных настройках: переключите эту опцию по своему усмотрению, это будет унаследовано для вновь созданых пользовательских конфигураций", "sv_SE": "Om det här alternativet är aktiverat i anpassade inställningar kommer den globala inmatningskonfigurationen att användas.\n\nI de globala inställningarna: du kan aktivera eller inaktivera det efter behov; den här inställningen kommer att ärvas av alla nya anpassade konfigurationer som skapas.", - "th_TH": "", + "th_TH": "หากเปิดใช้งานตัวเลือกนี้ในการตั้งค่าแบบกำหนดเอง การตั้งค่าการควบคุมแบบรวม (Global Input Configuration) จะถูกนำมาใช้\n\nในการตั้งค่าทั่วไป: คุณสามารถเปิดหรือปิดใช้งานได้ตามต้องการ โดยการตั้งค่านี้จะถูกใช้เป็นค่าเริ่มต้นสำหรับการตั้งค่าแบบกำหนดเองที่สร้างขึ้นใหม่ทุกอัน", "tr_TR": "", "uk_UA": "Якщо цей параметр увімкнено в користувацьких налаштуваннях, буде використовуватись глобальна конфігурація вводу.\n\nВ глобальних налаштуваннях: за потреби цю опцію можна вмикати або вимикати; це налаштування автоматично застосується до всіх нових конфігурацій, які ви створите.", "zh_CN": "如果在自定义设置中启用了此选项,则将使用全局输入配置。\n\n在全局设置中: 您可以根据需要启用或禁用它;之后创建的任何自定义配置都将继承此设置。", @@ -16385,7 +16385,7 @@ "pt_BR": "Sincroniza a data e hora do emulador com seu sistema PC", "ru_RU": "Повторно синхронизирует системное время, чтобы оно соответствовало текущей дате и времени вашего компьютера.", "sv_SE": "Återsynkronisera systemtiden för att matcha din dators aktuella datum och tid.", - "th_TH": "", + "th_TH": "ปรับเวลาในระบบให้ตรงกับวันที่และเวลาปัจจุบันของเครื่องคอมพิวเตอร์ของคุณ", "tr_TR": "", "uk_UA": "Синхронізувати системний час, щоб він відповідав поточній даті та часу вашого ПК.", "zh_CN": "重新同步系统时间以匹配您电脑的当前日期和时间。", @@ -16710,7 +16710,7 @@ "pt_BR": "A caixa de diálogo do Applet do controlador não aparecerá se o controle for desconectado enquanto um aplicativo estiver em execução.\n\nDeixe a opção DESLIGADO se não tiver certeza.", "ru_RU": "Диалоговое окно апплета контроллера не будет отображаться, если геймпад отключен во время работы приложения.\n\nОставьте выключенным, если не уверены.", "sv_SE": "Handkontroller-appleten kommer inte att visas om gamepaden är frånkopplad under tiden en applikation körs.\n\nLämna AV om du är osäker.", - "th_TH": "", + "th_TH": "กล่องโต้ตอบของ Controller Applet จะไม่ปรากฏขึ้น หากเกมแพดถูกถอดออกในขณะที่แอปพลิเคชันกำลังทำงานอยู่\n\nหากไม่แน่ใจ ให้ปิดการใช้งานไว้", "tr_TR": "", "uk_UA": "Діалогове вікно Аплету Контролера не з'явиться, якщо геймпад було відключено під час роботи програми.\n\nЗалиште вимкненим якщо не впевнені.", "zh_CN": "在应用程序运行时如果游戏手柄断开连接则不会显示控制器小程序对话框。\n\n如果不确定,请保持关闭状态。", @@ -17185,7 +17185,7 @@ "pt_BR": "Imprime mensagens de log do Avalonia (UI) no console.", "ru_RU": "Выводит сообщения журнала Avalonia (интерфейс) в консоли.", "sv_SE": "Skriver ut loggmeddelanden från Avalonia (användargränssnittet) i konsollen.", - "th_TH": "", + "th_TH": "แสดงข้อความบันทึก (log) ของ Avalonia (UI) ในคอนโซล", "tr_TR": "", "uk_UA": "Виводити повідомлення журналу Avalonia (UI) в консоль", "zh_CN": "在控制台显示 Avalonia (UI) 的日志信息", @@ -17710,7 +17710,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "{0} bilder/s ({1}ms)", - "th_TH": "", + "th_TH": "{0} FPS ({1}มิลลิวินาที)", "tr_TR": "", "uk_UA": "", "zh_CN": "{0} FPS ({1}毫秒)", @@ -17735,7 +17735,7 @@ "pt_BR": "", "ru_RU": "", "sv_SE": "{0} bilder/s ({1}ms), Turbo ({2}%)", - "th_TH": "", + "th_TH": "{0} FPS ({1} มิลลิวินาที), โหมดเทอร์โบ ({2}%)", "tr_TR": "", "uk_UA": "{0} FPS ({1}ms), Турбо ({2}%)", "zh_CN": "{0} FPS ({1}ms), 涡轮 ({2}%)", @@ -17760,7 +17760,7 @@ "pt_BR": "Atualização Disponível!", "ru_RU": "Доступно обновление!", "sv_SE": "Uppdatering finns tillgänglig!", - "th_TH": "", + "th_TH": "อัปเดตใหม่พร้อมใช้งาน!", "tr_TR": "", "uk_UA": "Доступне оновлення!", "zh_CN": "有可用的更新!", @@ -18260,7 +18260,7 @@ "pt_BR": "Empacotado: Versão {0}", "ru_RU": "Баднл: Версия {0}", "sv_SE": "Bundlad: Version {0}", - "th_TH": "Bundled: เวอร์ชั่น {0}", + "th_TH": "บันเดิล: เวอร์ชัน {0}", "tr_TR": "", "uk_UA": "Комплектні: Версія {0}", "zh_CN": "捆绑:版本 {0}", @@ -18285,7 +18285,7 @@ "pt_BR": "Empacotado:", "ru_RU": "Бандл:", "sv_SE": "Bundlad:", - "th_TH": "", + "th_TH": "บันเดิล:", "tr_TR": "", "uk_UA": "Комплектні:", "zh_CN": "捆绑:", @@ -18310,7 +18310,7 @@ "pt_BR": "Parcial", "ru_RU": "Частично", "sv_SE": "Delvis", - "th_TH": "", + "th_TH": "ยังไม่สมบูรณ์", "tr_TR": "", "uk_UA": "Часткові", "zh_CN": "分区", @@ -18335,7 +18335,7 @@ "pt_BR": "Não Reduzido", "ru_RU": "Не обрезан", "sv_SE": "Orörd", - "th_TH": "", + "th_TH": "ยังไม่ได้ตัดแต่ง", "tr_TR": "", "uk_UA": "Необрізані", "zh_CN": "没有瘦身的", @@ -18360,7 +18360,7 @@ "pt_BR": "Reduzido", "ru_RU": "Обрезан", "sv_SE": "Optimerad", - "th_TH": "", + "th_TH": "ตัดแต่งแล้ว", "tr_TR": "", "uk_UA": "Обрізані", "zh_CN": "经过瘦身的", @@ -18385,7 +18385,7 @@ "pt_BR": "Falhou", "ru_RU": "(Ошибка)", "sv_SE": "(misslyckades)", - "th_TH": "", + "th_TH": "(ล้มเหลว)", "tr_TR": "", "uk_UA": "(Невдача)", "zh_CN": "(失败)", @@ -18410,7 +18410,7 @@ "pt_BR": "Salvar {0:n0} Mb", "ru_RU": "Сохранить {0:n0} Мб", "sv_SE": "Spara {0:n0} Mb", - "th_TH": "", + "th_TH": "บันทึก {0:n0} เมกะไบต์", "tr_TR": "", "uk_UA": "Зберегти {0:n0} Мб", "zh_CN": "能节约 {0:n0} Mb", @@ -18435,7 +18435,7 @@ "pt_BR": "Salvo {0:n0} Mb", "ru_RU": "Сохранено {0:n0} Мб", "sv_SE": "Sparade {0:n0} Mb", - "th_TH": "", + "th_TH": "บันทึกแล้ว {0:n0} เมกะไบต์", "tr_TR": "", "uk_UA": "Збережено {0:n0} Мб", "zh_CN": "节约了 {0:n0} Mb", @@ -18610,7 +18610,7 @@ "pt_BR": "Diálogo do Gabinete", "ru_RU": "Сообщение кабинета", "sv_SE": "Cabinet-dialog", - "th_TH": "", + "th_TH": "กล่องโต้ตอบไฟล์ Cabinet", "tr_TR": "", "uk_UA": "Сповіщення Cabinet", "zh_CN": "档案对话框", @@ -18635,7 +18635,7 @@ "pt_BR": "Digite o novo nome do seu Amiibo", "ru_RU": "Введите новое имя вашего Amiibo", "sv_SE": "Ange nya namnet för din Amiibo", - "th_TH": "", + "th_TH": "กรุณาใส่ชื่อใหม่ของ Amiibo ของคุณ", "tr_TR": "", "uk_UA": "Вкажіть Ваше нове ім'я Amiibo", "zh_CN": "输入你的 Amiibo 的新名字", @@ -18660,7 +18660,7 @@ "pt_BR": "Por favor, escaneie seu Amiibo agora.", "ru_RU": "Пожалуйста, отсканируйте свой Amiibo.", "sv_SE": "Skanna din Amiibo nu.", - "th_TH": "", + "th_TH": "กรุณาสแกน Amiibo ของคุณตอนนี้", "tr_TR": "", "uk_UA": "Будь ласка, проскануйте Ваш Amiibo.", "zh_CN": "请现在扫描你的 Amiibo", @@ -19560,7 +19560,7 @@ "pt_BR": "Configurações de LED", "ru_RU": "Настройки LED-подсветки", "sv_SE": "LED-inställningar", - "th_TH": "", + "th_TH": "ตั้งค่าไฟ LED", "tr_TR": "", "uk_UA": "Налаштування LED", "zh_CN": "LED 设置", @@ -19660,7 +19660,7 @@ "pt_BR": "", "ru_RU": "Амибо", "sv_SE": "", - "th_TH": "", + "th_TH": "อมิโบ้", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -19835,7 +19835,7 @@ "pt_BR": "Verifique e Reduza o Arquivo XCI", "ru_RU": "Проверить и обрезать XCI файл", "sv_SE": "Kontrollera och optimera XCI-filer", - "th_TH": "", + "th_TH": "ตรวจสอบและตัดแต่งไฟล์ XCI", "tr_TR": "", "uk_UA": "Перевірити та Обрізати XCI файл", "zh_CN": "检查并瘦身 XCI 文件", @@ -19860,7 +19860,7 @@ "pt_BR": "Esta função primeiro verificará o espaço vazio e depois reduzirá o arquivo XCI para economizar espaço em disco.", "ru_RU": "Эта функция сначала проверит наличие пустого пространства, а затем обрежет файл XCI, чтобы сэкономить место на диске.", "sv_SE": "Denna funktion kommer först att kontrollera ledigt utrymme och sedan optimera XCI-filen för att spara diskutrymme.", - "th_TH": "", + "th_TH": "ฟังก์ชันนี้จะตรวจสอบพื้นที่ว่างก่อน จากนั้นจึงตัดแต่งไฟล์ XCI เพื่อประหยัดพื้นที่ดิสก์", "tr_TR": "", "uk_UA": "Ця функція спочатку перевірить наявність порожнього місця, після чого обріже файл XCI для економії місця на диску.", "zh_CN": "这个功能将会先检查 XCI 文件,再对其执行瘦身操作以节约磁盘空间。", @@ -19885,7 +19885,7 @@ "pt_BR": "Tamanho atual do arquivo: {0:n} MB\nTamanho dos dados do jogo: {1:n} MB\nEconomia de espaço em disco: {2:n} MB", "ru_RU": "Размер текущего файла: {0:n} Мб\nРазмер игровых данных: {1:n} MB\nЭкономия дискового пространства: {2:n} Мб", "sv_SE": "Aktuell filstorlek: {0:n} MB\nStorlek för speldata: {1:n} MB\nSparat diskutrymme: {2:n} MB", - "th_TH": "", + "th_TH": "ขนาดไฟล์ปัจจุบัน: {0:n} เมกะไบต์\nขนาดข้อมูลเกม: {1:n} เมกะไบต์\nพื้นที่ดิสก์ที่ประหยัดได้: {2:n} เมกะไบต์", "tr_TR": "", "uk_UA": "Поточний розмір файла: {0:n} MB\nРозмір файлів гри: {1:n} MB\nЕкономія місця: {2:n} MB", "zh_CN": "当前文件大小: {0:n} MB\n游戏数据大小: {1:n} MB\n节约的磁盘空间: {2:n} MB", @@ -19910,7 +19910,7 @@ "pt_BR": "O arquivo XCI não precisa ser reduzido. Verifique os logs para mais detalhes", "ru_RU": "Файл XCI не нуждается в обрезке. Проверьте логи для получения более подробной информации", "sv_SE": "XCI-filen behöver inte optimeras. Kontrollera loggen för mer information", - "th_TH": "", + "th_TH": "ไฟล์ XCI ไม่จำเป็นต้องถูกตัดแต่ง โปรดตรวจสอบบันทึก (logs) สำหรับรายละเอียดเพิ่มเติม", "tr_TR": "", "uk_UA": "XCI файл не потребує обрізання. Перевірте журнали (logs) для отримання додаткової інформації", "zh_CN": "XCI 文件不需要被瘦身。查看日志以获得更多细节。", @@ -19935,7 +19935,7 @@ "pt_BR": "O arquivo XCI reduzido não pode ser desfeito. Verifique os logs para mais detalhes", "ru_RU": "XCI файл не может быть обрезан. Проверьте логи для получения более подробной информации", "sv_SE": "XCI-filen kan inte avoptimeras. Kontrollera loggen för mer information", - "th_TH": "", + "th_TH": "ไฟล์ XCI ไม่สามารถคืนสภาพเดิม (untrimmed) ได้ โปรดตรวจสอบบันทึก (logs) สำหรับรายละเอียดเพิ่มเติม", "tr_TR": "", "uk_UA": "XCI файл не може бути обрізаний. Перевірте журнали (logs) для отримання додаткової інформації", "zh_CN": "XCI 文件不能被瘦身。查看日志以获得更多细节。", @@ -19960,7 +19960,7 @@ "pt_BR": "O arquivo XCI é somente leitura e não pôde ser tornado gravável. Verifique os logs para mais detalhes", "ru_RU": "Файл XCI доступен только для чтения и его невозможно сделать доступным для записи. Проверьте логи для получения более подробной информации", "sv_SE": "XCI-filen är skrivskyddad och kunde inte göras skrivbar. Kontrollera loggen för mer information", - "th_TH": "", + "th_TH": "ไฟล์ XCI เป็นแบบอ่านอย่างเดียว และไม่สามารถเปลี่ยนเป็นแบบเขียนได้ โปรดตรวจสอบบันทึก (logs) สำหรับรายละเอียดเพิ่มเติม", "tr_TR": "", "uk_UA": "XCI файл \"Тільки для Читання\" і не може бути прочитаним. Перевірте журнали (logs) для отримання додаткової інформації", "zh_CN": "XCI 文件是只读的,且不可以被标记为可读取的。查看日志以获得更多细节。", @@ -19985,7 +19985,7 @@ "pt_BR": "O arquivo XCI mudou de tamanho desde que foi escaneado. Verifique se o arquivo não está sendo gravado e tente novamente.", "ru_RU": "Файл XCI изменился в размере после сканирования. Проверьте, не производится ли запись в этот файл, и повторите попытку.", "sv_SE": "XCI-filen har ändrats i storlek sedan den lästes av. Kontrollera att filen inte skrivs till och försök igen.", - "th_TH": "", + "th_TH": "ขนาดไฟล์ XCI มีการเปลี่ยนแปลงตั้งแต่ที่สแกนครั้งล่าสุด กรุณาตรวจสอบว่าไฟล์ไม่ได้ถูกเขียนข้อมูล และลองใหม่อีกครั้ง", "tr_TR": "", "uk_UA": "Розмір файлу XCI змінився з моменту сканування. Перевірте, чи не записується файл, та спробуйте знову", "zh_CN": "XCI 文件在扫描后大小发生了变化。请检查文件是否未被写入,然后重试。", @@ -20010,7 +20010,7 @@ "pt_BR": "O arquivo XCI tem dados na área de espaço livre, não é seguro reduzi-lo", "ru_RU": "XCI файл содержит данные в пустой зоне, обрезать его небезопасно", "sv_SE": "XCI-filen har data i det lediga utrymmet. Den är inte säker att optimera", - "th_TH": "", + "th_TH": "ไฟล์ XCI มีข้อมูลในพื้นที่ว่าง จึงไม่ปลอดภัยที่จะทำการตัดแต่ง", "tr_TR": "", "uk_UA": "Файл XCI містить дані в зоні вільного простору, тому обрізка небезпечна", "zh_CN": "XCI 文件的空闲区域内有数据,不能安全瘦身。", @@ -20035,7 +20035,7 @@ "pt_BR": "O arquivo XCI contém dados inválidos. Verifique os logs para obter mais detalhes", "ru_RU": "Файл XCI содержит недопустимые данные. Проверьте логи для получения дополнительной информации", "sv_SE": "XCI-filen innehåller ogiltig data. Kontrollera loggen för mer information", - "th_TH": "", + "th_TH": "ไฟล์ XCI มีข้อมูลที่ไม่ถูกต้อง โปรดตรวจสอบบันทึก (logs) สำหรับรายละเอียดเพิ่มเติม", "tr_TR": "", "uk_UA": "XCI Файл містить недійсні дані. Перевірте журнали (logs) для отримання додаткової інформації", "zh_CN": "XCI 文件含有无效数据。查看日志以获得更多细节。", @@ -20060,7 +20060,7 @@ "pt_BR": "O arquivo XCI não pôde ser aberto para gravação. Verifique os logs para mais detalhes", "ru_RU": "XCI файл не удалось открыть для записи. Проверьте логи для получения дополнительной информации", "sv_SE": "XCI-filen kunde inte öppnas för skrivning. Kontrollera loggen för mer information", - "th_TH": "", + "th_TH": "ไม่สามารถเปิดไฟล์ XCI เพื่อเขียนข้อมูลได้ โปรดตรวจสอบบันทึก (logs) สำหรับรายละเอียดเพิ่มเติม", "tr_TR": "", "uk_UA": "XCI Файл файл не вдалося відкрити для запису. Перевірте журнали для додаткової інформації", "zh_CN": "XCI 文件不能被读写。查看日志以获得更多细节。", @@ -20085,7 +20085,7 @@ "pt_BR": "A redução do arquivo XCI falhou", "ru_RU": "Обрезка файла XCI не удалась", "sv_SE": "Optimering av XCI-filen misslyckades", - "th_TH": "", + "th_TH": "การตัดแต่งไฟล์ XCI ล้มเหลว", "tr_TR": "", "uk_UA": "Не вдалося обрізати файл XCI", "zh_CN": "XCI 文件瘦身失败", @@ -20110,7 +20110,7 @@ "pt_BR": "A operação foi cancelada", "ru_RU": "Операция была отменена", "sv_SE": "Åtgärden avbröts", - "th_TH": "", + "th_TH": "การดำเนินการถูกยกเลิกแล้ว", "tr_TR": "", "uk_UA": "Операція перервана", "zh_CN": "操作已取消", @@ -20135,7 +20135,7 @@ "pt_BR": "Nenhuma operação foi realizada", "ru_RU": "Операция не была проведена", "sv_SE": "Ingen åtgärd genomfördes", - "th_TH": "", + "th_TH": "ไม่มีการดำเนินการใด ๆ ถูกทำขึ้น", "tr_TR": "", "uk_UA": "Операція не проводилася", "zh_CN": "未执行操作", @@ -20285,7 +20285,7 @@ "pt_BR": "Redutor de Arquivo XCI", "ru_RU": "Уменьшение размера XCI файлов", "sv_SE": "Optimera XCI-filer", - "th_TH": "", + "th_TH": "เครื่องมือตัดแต่งไฟล์ XCI", "tr_TR": "", "uk_UA": "Обрізка XCI Файлів", "zh_CN": "XCI 文件瘦身器", @@ -20310,7 +20310,7 @@ "pt_BR": "{0} de {1} Título(s) Selecionado(s)", "ru_RU": "{0} из {1} файла(ов) выбрано", "sv_SE": "{0} av {1} spel markerade", - "th_TH": "", + "th_TH": "เลือก {0} จาก {1} รายการ", "tr_TR": "", "uk_UA": "{0} з {1} тайтл(ів) обрано", "zh_CN": "在 {1} 中选中了 {0} 个游戏 ", @@ -20335,7 +20335,7 @@ "pt_BR": "{0} de {1} Título(s) Selecionado(s) ({2} exibidos)", "ru_RU": "{0} из {1} файла(ов) выбрано ({2} показано)", "sv_SE": "{0} av {1} spel markerade ({2} visade)", - "th_TH": "", + "th_TH": "เลือก {0} จาก {1} รายการ (แสดงผล {2} รายการ)", "tr_TR": "", "uk_UA": "{0} з {1} тайтл(ів) обрано ({2} відображається)", "zh_CN": "在 {1} 中选中了 {0} 个游戏 (显示了 {2} 个)", @@ -20360,7 +20360,7 @@ "pt_BR": "Reduzindo {0} Título(s)...", "ru_RU": "Обрезка {0} файла(ов)...", "sv_SE": "Optimerar {0} spel...", - "th_TH": "", + "th_TH": "กำลังตัดแต่ง {0} รายการ...", "tr_TR": "", "uk_UA": "Обрізка {0} тайтл(ів)...", "zh_CN": "{0} 个游戏瘦身中。。。", @@ -20385,7 +20385,7 @@ "pt_BR": "Desfazendo redução {0} Título(s)...", "ru_RU": "Отмена обрезки {0} файла(ов)...", "sv_SE": "Avoptimerar {0} spel...", - "th_TH": "", + "th_TH": "กำลังยกเลิกการตัดแต่ง {0} รายการ...", "tr_TR": "", "uk_UA": "Необрізаних {0} тайтл(ів)...", "zh_CN": "正在精简 {0} 个游戏", @@ -20410,7 +20410,7 @@ "pt_BR": "Falhou", "ru_RU": "Ошибка", "sv_SE": "Misslyckades", - "th_TH": "", + "th_TH": "ล้มเหลว", "tr_TR": "", "uk_UA": "Невдача", "zh_CN": "失败", @@ -20435,7 +20435,7 @@ "pt_BR": "Economia Potencial", "ru_RU": "Потенциально освобождено места", "sv_SE": "Möjlig besparning", - "th_TH": "", + "th_TH": "พื้นที่ประหยัดได้", "tr_TR": "", "uk_UA": "Потенційна економія", "zh_CN": "潜在的储存空间节省", @@ -20460,7 +20460,7 @@ "pt_BR": "Economia Real", "ru_RU": "Реально освобождено места", "sv_SE": "Faktisk besparning", - "th_TH": "", + "th_TH": "พื้นที่ที่ประหยัดจริง", "tr_TR": "", "uk_UA": "Зекономлено", "zh_CN": "实际的储存空间节省", @@ -20485,7 +20485,7 @@ "pt_BR": "", "ru_RU": "{0:n0} Мб", "sv_SE": "", - "th_TH": "", + "th_TH": "{0:n0} เมกะไบต์", "tr_TR": "", "uk_UA": "{0:n0} Мб", "zh_CN": "", @@ -20510,7 +20510,7 @@ "pt_BR": "Marcar Todos", "ru_RU": "Выбрать то что показано", "sv_SE": "Markera visade", - "th_TH": "", + "th_TH": "เลือกรายการที่แสดง", "tr_TR": "", "uk_UA": "Вибрати показані", "zh_CN": "选定显示的", @@ -20535,7 +20535,7 @@ "pt_BR": "Desmarcar Todos", "ru_RU": "Отменить выбор показанного", "sv_SE": "Avmarkera visade", - "th_TH": "", + "th_TH": "ยกเลิกการเลือกรายการที่แสดง", "tr_TR": "", "uk_UA": "Скасувати вибір показаного", "zh_CN": "反选显示的", @@ -20560,7 +20560,7 @@ "pt_BR": "Título", "ru_RU": "Приложение", "sv_SE": "Titel", - "th_TH": "", + "th_TH": "ชื่อไฟล์", "tr_TR": "", "uk_UA": "Назва", "zh_CN": "标题", @@ -20585,7 +20585,7 @@ "pt_BR": "Economia de Espaço", "ru_RU": "Сохранение места на диске", "sv_SE": "Utrymmesbesparning", - "th_TH": "", + "th_TH": "ลดการใช้พื้นที่", "tr_TR": "", "uk_UA": "Економія місця", "zh_CN": "节省空间", @@ -20610,7 +20610,7 @@ "pt_BR": "Reduzir XCI", "ru_RU": "Обрезать", "sv_SE": "Optimera", - "th_TH": "", + "th_TH": "ตัดแต่ง", "tr_TR": "", "uk_UA": "Обрізка", "zh_CN": "瘦身", @@ -20635,7 +20635,7 @@ "pt_BR": "Desfazer Redução", "ru_RU": "Отмена обрезки", "sv_SE": "Avoptimera", - "th_TH": "", + "th_TH": "ยกเลิกการตัดแต่ง", "tr_TR": "", "uk_UA": "Зшивання", "zh_CN": "取消精简", @@ -20660,7 +20660,7 @@ "pt_BR": "{0} nova(s) atualização(ões) adicionada(s)", "ru_RU": "Добавлено {0} новых обновлений", "sv_SE": "{0} nya uppdatering(ar) lades till", - "th_TH": "{0} อัพเดตที่เพิ่มมาใหม่", + "th_TH": "มีอัปเดตใหม่เข้ามา {0} รายการ", "tr_TR": "", "uk_UA": "{0} нових оновлень додано", "zh_CN": "{0} 个更新被添加", @@ -20735,7 +20735,7 @@ "pt_BR": "ID da Build:", "ru_RU": "ID версии:", "sv_SE": "Bygg-id:", - "th_TH": "รหัสการสร้าง:", + "th_TH": "หมายเลขบิลด์:", "tr_TR": "", "uk_UA": "ID збірки:", "zh_CN": "游戏版本 ID:", @@ -20785,7 +20785,7 @@ "pt_BR": "{0} DLC(s) disponíveis", "ru_RU": "{0} доступных DLC", "sv_SE": "{0} DLC(er) tillgängliga", - "th_TH": "", + "th_TH": "มี DLC จำนวน {0} รายการ", "tr_TR": "", "uk_UA": "{0} DLC доступно", "zh_CN": "{0} 个 DLC", @@ -20810,7 +20810,7 @@ "pt_BR": "{0} novo(s) conteúdo(s) para download adicionado(s)", "ru_RU": "Добавлено {0} новых DLC", "sv_SE": "{0} nya hämtningsbara innehåll lades till", - "th_TH": "{0} DLC ใหม่ที่เพิ่มเข้ามา", + "th_TH": "มีเนื้อหาเสริมใหม่ {0} รายการ", "tr_TR": "", "uk_UA": "{0} нового завантажувального вмісту додано", "zh_CN": "{0} 个 DLC 被添加", @@ -20835,7 +20835,7 @@ "pt_BR": "{0} novo(s) conteúdo(s) para download adicionado(s)", "ru_RU": "Добавлено {0} новых DLC", "sv_SE": "{0} nya hämtningsbara innehåll lades till", - "th_TH": "{0} ใหม่ที่เพิ่มเข้ามา", + "th_TH": "เพิ่มเนื้อหาเสริมดาวน์โหลดใหม่ {0} รายการ", "tr_TR": "", "uk_UA": "{0} нового завантажувального вмісту додано", "zh_CN": "{0} 个 DLC 被添加", @@ -20860,7 +20860,7 @@ "pt_BR": "{0} conteúdo(s) para download ausente(s) removido(s)", "ru_RU": "{0} отсутствующих DLC удалено", "sv_SE": "{0} saknade hämtningsbara innehåll togs bort", - "th_TH": "", + "th_TH": "ลบเนื้อหาเสริมที่หายไปจำนวน {0} รายการ", "tr_TR": "", "uk_UA": "{0} відсутнього завантажувального вмісту видалено", "zh_CN": "{0} 个失效的 DLC 已移除", @@ -20885,7 +20885,7 @@ "pt_BR": "{0} nova(s) atualização(ões) adicionada(s)", "ru_RU": "{0} новых обновлений добавлено", "sv_SE": "{0} nya uppdatering(ar) lades till", - "th_TH": "{0} อัพเดตใหม่ที่เพิ่มเข้ามา", + "th_TH": "เพิ่มการอัปเดตใหม่ {0} รายการ", "tr_TR": "", "uk_UA": "{0} нових оновлень додано", "zh_CN": "{0} 个游戏更新被添加", @@ -20910,7 +20910,7 @@ "pt_BR": "{0} atualização(ões) ausente(s) removida(s)", "ru_RU": "{0} отсутствующих обновлений удалено", "sv_SE": "{0} saknade uppdatering(ar) togs bort", - "th_TH": "", + "th_TH": "ลบการอัปเดตที่หายไปจำนวน {0} รายการ", "tr_TR": "", "uk_UA": "{0} відсутніх оновлень видалено", "zh_CN": "{0} 个失效的游戏更新已移除", @@ -20985,7 +20985,7 @@ "pt_BR": "Continuar", "ru_RU": "Продолжить", "sv_SE": "Fortsätt", - "th_TH": "", + "th_TH": "ดำเนินการต่อ", "tr_TR": "", "uk_UA": "Продовжити", "zh_CN": "继续", @@ -21360,7 +21360,7 @@ "pt_BR": "Automático", "ru_RU": "Авто", "sv_SE": "Automatiskt", - "th_TH": "", + "th_TH": "อัตโนมัติ", "tr_TR": "", "uk_UA": "Автоматично", "zh_CN": "自动", @@ -21385,7 +21385,7 @@ "pt_BR": "Usa Vulkan.\nEm um Mac ARM, e ao jogar um jogo que roda bem nele, usa o renderizador Metal.", "ru_RU": "Использует Vulkan.\nНа Mac с ARM процессорами используется Metal, если игра с ним совместима и хорошо работает.", "sv_SE": "Använder Vulkan.\nPå en ARM Mac och vid spel som körs bra på den så används Metal-bakänden.", - "th_TH": "", + "th_TH": "ใช้ Vulkan\nบน Mac (ARM) จะใช้ Metal หากเกมทำงานได้ดีกว่า", "tr_TR": "", "uk_UA": "Використовує Vulkan.\nНа Mac з процесором ARM використовується графічний API Metal, якщо гра під нього оптимізована.", "zh_CN": "使用 Vulkan。\n在 ARM Mac 上,当玩在其下运行良好的游戏时,使用 Metal 后端。", @@ -22185,7 +22185,7 @@ "pt_BR": "", "ru_RU": "Билинейная", "sv_SE": "Bilinjär", - "th_TH": "", + "th_TH": "บิไลเนียร์", "tr_TR": "", "uk_UA": "Білінійний (Bilinear)", "zh_CN": "Bilinear(双线性过滤)", @@ -22210,7 +22210,7 @@ "pt_BR": "Mais Próximo", "ru_RU": "Ступенчатая", "sv_SE": "Närmaste", - "th_TH": "ใกล้สุด", + "th_TH": "นีเรสต์", "tr_TR": "", "uk_UA": "Найближчий (Nearest)", "zh_CN": "Nearest(邻近过滤)", @@ -22235,7 +22235,7 @@ "pt_BR": null, "ru_RU": null, "sv_SE": null, - "th_TH": null, + "th_TH": "FSR (เพิ่มความคมชัดภาพ)", "tr_TR": null, "uk_UA": null, "zh_CN": "FSR(超级分辨率锐画技术)", @@ -22260,7 +22260,7 @@ "pt_BR": "Área", "ru_RU": "Зональная", "sv_SE": "Yta", - "th_TH": "", + "th_TH": "พื้นที่", "tr_TR": "", "uk_UA": "Зональна", "zh_CN": "Area(区域过滤)", @@ -22335,7 +22335,7 @@ "pt_BR": "SMAA Baixo", "ru_RU": "SMAA Низкое", "sv_SE": "SMAA låg", - "th_TH": "SMAA ต่ำ", + "th_TH": "ลดรอยหยัก (SMAA ต่ำ)", "tr_TR": "Düşük SMAA", "uk_UA": "SMAA Низький", "zh_CN": "SMAA 低质量", @@ -22360,7 +22360,7 @@ "pt_BR": "SMAA Médio", "ru_RU": "SMAA Среднее", "sv_SE": "SMAA medium", - "th_TH": "SMAA ปานกลาง", + "th_TH": "ลดรอยหยัก (SMAA ปานกลาง)", "tr_TR": "Orta SMAA", "uk_UA": "SMAA Середній", "zh_CN": "SMAA 中质量", @@ -22385,7 +22385,7 @@ "pt_BR": "SMAA Alto", "ru_RU": "SMAA Высокое", "sv_SE": "SMAA hög", - "th_TH": "SMAA สูง", + "th_TH": "ลดรอยหยัก (SMAA สูง)", "tr_TR": "Yüksek SMAA", "uk_UA": "SMAA Високий", "zh_CN": "SMAA 高质量", @@ -22410,7 +22410,7 @@ "pt_BR": "", "ru_RU": "SMAA Ультра", "sv_SE": "SMAA ultra", - "th_TH": "SMAA สูงมาก", + "th_TH": "ลดรอยหยัก (SMAA สูงทาก)", "tr_TR": "En Yüksek SMAA", "uk_UA": "SMAA Ультра", "zh_CN": "SMAA 超高质量", @@ -22585,7 +22585,7 @@ "pt_BR": "Ver Registro", "ru_RU": "", "sv_SE": "Visa ändringslogg", - "th_TH": "ด", + "th_TH": "ตรวจสอบการเปลี่ยนแปลงเวอร์ชัน", "tr_TR": "", "uk_UA": "Показати список змін", "zh_CN": "查看更新日志", @@ -22785,7 +22785,7 @@ "pt_BR": "Desabilitar hospedagem de rede P2P (pode aumentar a latência)", "ru_RU": "Отключить хостинг P2P-сетей (может увеличить задержку)", "sv_SE": "Inaktivera P2P-nätverkshosting (kan öka latens)", - "th_TH": "", + "th_TH": "ปิดการโฮสต์เครือข่ายแบบ P2P (อาจเพิ่มความหน่วง)", "tr_TR": "", "uk_UA": "Вимкнути хостинг P2P мережі (може збільшити затримку)", "zh_CN": "禁用 P2P 网络连接 (也许会增加延迟)", @@ -22810,7 +22810,7 @@ "pt_BR": "Desabilite a hospedagem de rede P2P, os pares farão proxy através do servidor mestre em vez de se conectarem a você diretamente.", "ru_RU": "Отключая хостинг P2P-сетей, пользователи будут проксироваться через главный сервер, а не подключаться к вам напрямую.", "sv_SE": "Inaktivera P2P-nätverkshosting, motparter kommer skickas genom masterservern isället för att ansluta direkt till dig.", - "th_TH": "", + "th_TH": "ปิดการโฮสต์เครือข่ายแบบ P2P โดยที่ผู้เล่นคนอื่นจะเชื่อมต่อผ่านเซิร์ฟเวอร์กลางแทนที่จะเชื่อมต่อโดยตรงกับคุณ", "tr_TR": "", "uk_UA": "Вимкнути хостинг P2P мережі, піри будуть підключатися через майстер-сервер замість прямого з'єднання з вами.", "zh_CN": "禁用 P2P 网络连接,对方将通过主服务器进行连接,而不是直接连接到您。", @@ -22835,7 +22835,7 @@ "pt_BR": "Senha de Rede:", "ru_RU": "Cетевой пароль:", "sv_SE": "Lösenfras för nätverk:", - "th_TH": "", + "th_TH": "รหัสผ่านสำหรับเครือข่าย:", "tr_TR": "", "uk_UA": "Мережевий пароль:", "zh_CN": "网络密码:", @@ -22860,7 +22860,7 @@ "pt_BR": "Você só poderá ver jogos hospedados com a mesma senha que você.", "ru_RU": "Вы сможете видеть только те игры, в которых используется тот же пароль, что и у вас.", "sv_SE": "Du kommer endast kunna se hostade spel med samma lösenfras som du.", - "th_TH": "", + "th_TH": "คุณจะสามารถเห็นเกมที่โฮสต์โดยใช้รหัสผ่านเครือข่ายเดียวกับคุณเท่านั้น", "tr_TR": "", "uk_UA": "Ви зможете бачити лише ті ігри, які мають такий самий пароль, як і у вас.", "zh_CN": "您只能看到与您使用相同密码的游戏房间。", @@ -22885,7 +22885,7 @@ "pt_BR": "Insira uma frase-senha no formato Ryujinx-<8 hex chars>. Você só poderá ver jogos hospedados com a mesma frase-senha que você.", "ru_RU": "Введите пароль в формате Ryujinx-<8 шестнадцатеричных символов>. Вы сможете видеть только те игры, в которых используется тот же пароль, что и у вас.", "sv_SE": "Ange en lösenfras i formatet Ryujinx-<8 hextecken>. Du kommer endast kunna se hostade spel med samma lösenfras som du.", - "th_TH": "", + "th_TH": "กรุณาใส่รหัสผ่านในรูปแบบ Ryujinx-<8 หลักเลขฐานสิบหก> คุณจะสามารถเห็นเกมที่โฮสต์โดยใช้รหัสผ่านเดียวกับคุณเท่านั้น", "tr_TR": "", "uk_UA": "Введіть пароль у форматі Ryujinx-<8 символів>. Ви зможете бачити лише ті ігри, які мають такий самий пароль, як і у вас.", "zh_CN": "以 Ryujinx-<8个十六进制字符> 的格式输入密码。您只能看到与您使用相同密码的游戏房间。", @@ -22910,7 +22910,7 @@ "pt_BR": "(público)", "ru_RU": "(публичный)", "sv_SE": "(publik)", - "th_TH": "", + "th_TH": "(สาธารณะ)", "tr_TR": "", "uk_UA": "(публічний)", "zh_CN": "(公开的)", @@ -22935,7 +22935,7 @@ "pt_BR": "Gerar Aleatório", "ru_RU": "Сгенерировать рандомно", "sv_SE": "Generera slumpmässigt", - "th_TH": "", + "th_TH": "สร้างแบบสุ่มขึ้นมา", "tr_TR": "", "uk_UA": "Згенерувати випадкову", "zh_CN": "随机生成", @@ -22960,7 +22960,7 @@ "pt_BR": "Gera uma nova senha, que pode ser compartilhada com outros jogadores.", "ru_RU": "Генерирует новый пароль, который можно передать другим игрокам.", "sv_SE": "Genererar en ny lösenfras som kan delas med andra spelare.", - "th_TH": "", + "th_TH": "สร้างรหัสผ่านใหม่ ซึ่งสามารถแชร์กับผู้เล่นคนอื่นได้", "tr_TR": "", "uk_UA": "Генерує новий пароль, яким можна поділитися з іншими гравцями.", "zh_CN": "生成一个新的密码,可以与其他玩家共享。", @@ -22985,7 +22985,7 @@ "pt_BR": "Limpar", "ru_RU": "Очистить", "sv_SE": "Töm", - "th_TH": "", + "th_TH": "ล้าง", "tr_TR": "", "uk_UA": "Очистити", "zh_CN": "清除", @@ -23010,7 +23010,7 @@ "pt_BR": "Limpa a senha atual, retornando à rede pública.", "ru_RU": "Очищает текущий пароль, возвращаясь в публичную сеть.", "sv_SE": "Tömmer aktuell lösenfras och återgår till det publika nätverket.", - "th_TH": "", + "th_TH": "ลบรหัสผ่านปัจจุบัน เพื่อกลับไปใช้เครือข่ายสาธารณะ", "tr_TR": "", "uk_UA": "Очищає поточну пароль, повертаючись до публічної мережі.", "zh_CN": "清除当前密码,返回公共网络。", @@ -23035,7 +23035,7 @@ "pt_BR": "Frase-senha inválida! Deve estar no formato \"Ryujinx-<8 hex chars>\"", "ru_RU": "Неверный пароль! Пароль должен быть в формате «Ryujinx-<8 шестнадцатеричных символов>»", "sv_SE": "Ogiltig lösenfras! Måste vara i formatet \"Ryujinx-<8 hextecken>\"", - "th_TH": "", + "th_TH": "รหัสผ่านไม่ถูกต้อง! ต้องอยู่ในรูปแบบ \"Ryujinx-<8 หลักเลขฐานสิบหก>\"", "tr_TR": "", "uk_UA": "Невірний пароль! Має бути в форматі \"Ryujinx-<8 символів>\"", "zh_CN": "无效密码!密码的格式必须是\"Ryujinx-<8个十六进制字符>\"", @@ -23060,7 +23060,7 @@ "pt_BR": "", "ru_RU": "Вертикальная синхронизация:", "sv_SE": "", - "th_TH": "", + "th_TH": "วีซิงค์:", "tr_TR": "", "uk_UA": "Вертикальна синхронізація (VSync):", "zh_CN": "垂直同步(VSync)", @@ -23085,7 +23085,7 @@ "pt_BR": "Habilitar taxa de atualização personalizada (Experimental)", "ru_RU": "Включить пользовательскую частоту кадров (Экспериментально)", "sv_SE": "Aktivera anpassad uppdateringsfrekvens (experimentell)", - "th_TH": "", + "th_TH": "เปิดใช้อัตรารีเฟรชแบบกำหนดเอง (ทดลองใช้)", "tr_TR": "", "uk_UA": "Увімкнути користувацьку частоту оновлення (Експериментально)", "zh_CN": "启动自定义刷新率(实验性功能)", @@ -23110,7 +23110,7 @@ "pt_BR": "", "ru_RU": "Консоль", "sv_SE": "", - "th_TH": "", + "th_TH": "เปลี่ยนโหมด", "tr_TR": "", "uk_UA": "", "zh_CN": "", @@ -23135,7 +23135,7 @@ "pt_BR": "Ilimitado", "ru_RU": "Без ограничений", "sv_SE": "Obunden", - "th_TH": "", + "th_TH": "ไม่มีข้อจำกัด", "tr_TR": "", "uk_UA": "Необмежена", "zh_CN": "无限制", @@ -23160,7 +23160,7 @@ "pt_BR": "Taxa de Atualização Personalizada", "ru_RU": "Пользовательская частота кадров", "sv_SE": "Anpassad uppdateringsfrekvens", - "th_TH": "", + "th_TH": "อัตรารีเฟรชแบบกำหนดเอง", "tr_TR": "", "uk_UA": "Користувацька", "zh_CN": "自定义刷新率", @@ -23185,7 +23185,7 @@ "pt_BR": "Sincronização vertical emulada. 'Switch' emula a taxa de atualização de 60 Hz do Switch. 'Ilimitada' é uma taxa de atualização sem limite.", "ru_RU": "Эмулированная вертикальная синхронизация. «Консоль» эмулирует частоту обновления консоли, равную 60 Гц. «Без ограничений» — неограниченная частота кадров.", "sv_SE": "Emulerad vertikal synk. 'Switch' emulerar Switchens uppdateringsfrekvens på 60Hz. 'Obunden' är en obegränsad uppdateringsfrekvens.", - "th_TH": "", + "th_TH": "การซิงค์ภาพแนวตั้งแบบจำลอง ('Emulated Vertical Sync') 'Switch' จำลองอัตรารีเฟรชของเครื่อง Nintendo Switch ที่ 60Hz 'Unbounded' คืออัตรารีเฟรชที่ไม่จำกัด", "tr_TR": "", "uk_UA": "Емульована вертикальна синхронізація кадрів. \"Switch\" емулює частоту оновлення консолі Nintendo Switch (60 Гц). \"Необмежена\" — частота оновлення не матиме обмежень.", "zh_CN": "模拟垂直同步。“Switch”模拟了 Switch 的 60Hz 刷新率。“无限制”没有刷新率限制。", @@ -23210,7 +23210,7 @@ "pt_BR": "Sincronização Vertical Emulada. 'Switch' emula a taxa de atualização de 60 Hz do Switch. 'Ilimitada' é uma taxa de atualização sem limite. 'Taxa de atualização personalizada' emula a taxa de atualização personalizada especificada.", "ru_RU": "Эмулированная вертикальная синхронизация. «Консоль» эмулирует частоту обновления консоли, равную 60 Гц. «Без ограничений» — неограниченная частота кадров. «Пользовательска частота кадров» эмулирует выбранную пользователем частоту кадров.", "sv_SE": "Emulerad vertikal synk. 'Switch' emulerar Switchens uppdateringsfrekvens på 60Hz. 'Obunden' är en obegränsad uppdateringsfrekvens. 'Anpassad uppdateringsfrekvens' emulerar den angivna anpassade uppdateringsfrekvensen.", - "th_TH": "", + "th_TH": "การซิงค์ภาพแนวตั้งแบบจำลอง การซิงค์ภาพแนวตั้งแบบจำลอง 'Unbounded' คืออัตรารีเฟรชที่ไม่จำกัด 'Custom Refresh Rate' จำลองอัตรารีเฟรชแบบกำหนดเองที่ระบุไว้", "tr_TR": "", "uk_UA": "Емульована вертикальна синхронізація кадрів. \"Switch\" емулює частоту оновлення консолі Nintendo Switch (60 Гц). \"Необмежена\" — частота оновлення не матиме обмежень. \"Користувацька\" емулює вказану користувачем частоту оновлення.", "zh_CN": "模拟垂直同步。“Switch”模拟了 Switch 的 60Hz 刷新率。“无限制”没有刷新率限制。“自定义刷新率”模拟指定的自定义刷新率。", @@ -23235,7 +23235,7 @@ "pt_BR": "Permite que o usuário especifique uma taxa de atualização emulada. Em alguns títulos, isso pode acelerar ou desacelerar a taxa de lógica do jogo. Em outros títulos, pode permitir limitar o FPS em algum múltiplo da taxa de atualização ou levar a um comportamento imprevisível. Este é um recurso experimental, sem garantias de como o jogo será afetado. \n\nDeixe OFF se não tiver certeza.", "ru_RU": "Позволяет пользователю указать эмулируемую частоту кадров. В некоторых играх это может ускорить или замедлить скорость логики игрового процесса. В других играх это может позволить ограничить FPS на уровне, кратном частоте обновления, или привести к непредсказуемому поведению. Это экспериментальная функция, и нет никаких гарантий того, как она повлияет на игровой процесс. \n\nОставьте выключенным, если не уверены.", "sv_SE": "Låter användaren ange en emulerad uppdateringsfrekvens. För vissa spel så kan detta snabba upp eller ner frekvensen för spellogiken. I andra spel så kan detta tillåta att bildfrekvensen kapas för delar av uppdateringsfrekvensen eller leda till oväntat beteende. Detta är en experimentell funktion utan några garantier för hur spelet påverkas. \n\nLämna AV om du är osäker.", - "th_TH": "", + "th_TH": "อนุญาตให้ผู้ใช้กำหนดอัตรารีเฟรชแบบจำลองได้ ในบางเกมอาจทำให้เกมทำงานเร็วขึ้นหรือช้าลงตามตรรกะของเกม ในบางเกมอาจช่วยจำกัด FPS ให้อยู่ที่ค่าคูณของอัตรารีเฟรช หรืออาจทำให้เกิดพฤติกรรมที่ไม่คาดคิดได้ ฟีเจอร์นี้ยังเป็นการทดลอง และไม่มีการรับประกันว่าการเล่นเกมจะได้รับผลกระทบอย่างไร\n\nปล่อยปิดไว้ถ้าไม่แน่ใจ", "tr_TR": "", "uk_UA": "Дозволяє користувачу вказати емульовану частоту оновлення. У деяких іграх це може прискорити або сповільнити логіку гри. Натомість в інших іграх ця функція може дозволити обмежити FPS на певні кратні частоти оновлення або призвести до непередбачуваної поведінки. Це експериментальна функція, без гарантій того, як вона вплине на ігровий процес. \n\nЗалиште ВИМКНЕНИМ, якщо не впевнені.", "zh_CN": "允许用户指定模拟刷新率。在某些游戏中,这可能会加快或减慢游戏逻辑的速度。在其他游戏中,它可能允许将 FPS 限制在刷新率的某个倍数,或者导致不可预测的行为。这是一个实验性功能,无法保证游戏会受到怎样的影响。\n\n如果不确定,请关闭。", @@ -23260,7 +23260,7 @@ "pt_BR": "O valor de meta da taxa de atualização personalizada.", "ru_RU": "Заданное значение частоты кадров", "sv_SE": "Målvärde för anpassad uppdateringsfrekvens.", - "th_TH": "", + "th_TH": "ค่าตั้งเป้าอัตรารีเฟรชแบบกำหนดเอง", "tr_TR": "", "uk_UA": "Цільове значення користувацької частоти оновлення.", "zh_CN": "目标自定义刷新率值。", @@ -23285,7 +23285,7 @@ "pt_BR": "A taxa de atualização personalizada, como uma porcentagem da taxa de atualização normal do Switch.", "ru_RU": "Пользовательская частота кадров в процентах от обычной частоты обновления на консоли.", "sv_SE": "Anpassad uppdateringsfrekvens, som en procentdel av den normala uppdateringsfrekvensen för Switch.", - "th_TH": "", + "th_TH": "อัตรารีเฟรชแบบกำหนดเอง ในรูปแบบเปอร์เซ็นต์ของอัตรารีเฟรชปกติของเครื่อง Switch", "tr_TR": "", "uk_UA": "Користувацька частота оновлення, як відсоток від стандартної частоти оновлення Switch.", "zh_CN": "自定义刷新率,占正常SWitch刷新率的百分比值。", @@ -23310,7 +23310,7 @@ "pt_BR": "Taxa de Atualização Personalizada %:", "ru_RU": "Пользовательская частота кадров %:", "sv_SE": "Anpassad uppdateringsfrekvens %:", - "th_TH": "", + "th_TH": "เปอร์เซ็นต์อัตรารีเฟรชแบบกำหนดเอง:", "tr_TR": "", "uk_UA": "Користувацька частота оновлення %:", "zh_CN": "自定义刷新率值 %:", @@ -23335,7 +23335,7 @@ "pt_BR": "Valor da Taxa de Atualização Personalizada:", "ru_RU": "Значение пользовательской частоты кадров:", "sv_SE": "Värde för anpassad uppdateringsfrekvens:", - "th_TH": "", + "th_TH": "ค่าของอัตรารีเฟรชแบบกำหนดเอง:", "tr_TR": "", "uk_UA": "Значення користувацької частоти оновлення:", "zh_CN": "自定义刷新率值:", @@ -23360,7 +23360,7 @@ "pt_BR": "Intervalo", "ru_RU": "Интервал", "sv_SE": "Intervall", - "th_TH": "", + "th_TH": "ความถี่", "tr_TR": "", "uk_UA": "Інтервал", "zh_CN": "间隔", @@ -23385,7 +23385,7 @@ "pt_BR": "Alternar Modo VSync:", "ru_RU": "Выбрать режим вертикальной синхронизации:", "sv_SE": "Växla VSync-läge:", - "th_TH": "", + "th_TH": "สลับโหมด วีซิงค์:", "tr_TR": "", "uk_UA": "Перемкнути VSync режим:", "zh_CN": "设置 VSync 模式:", @@ -23410,7 +23410,7 @@ "pt_BR": "Aumentar Taxa de Atualização:", "ru_RU": "Повышение пользовательской частоты кадров", "sv_SE": "Höj anpassad uppdateringsfrekvens", - "th_TH": "", + "th_TH": "เพิ่มอัตรารีเฟรชแบบกำหนดเอง", "tr_TR": "", "uk_UA": "Підвищити користувацьку частоту оновлення", "zh_CN": "提高自定义刷新率:", @@ -23435,7 +23435,7 @@ "pt_BR": "Reduzir Taxa de Atualização:", "ru_RU": "Понижение пользовательской частоты кадров", "sv_SE": "Sänk anpassad uppdateringsfrekvens", - "th_TH": "", + "th_TH": "ลดอัตรารีเฟรชแบบกำหนดเอง:", "tr_TR": "", "uk_UA": "Понизити користувацьку частоту оновлення", "zh_CN": "降低自定义刷新率:", @@ -23460,7 +23460,7 @@ "pt_BR": "Modo Turbo:", "ru_RU": "", "sv_SE": "Turboläge:", - "th_TH": "", + "th_TH": "โหมดเทอร์โบ:", "tr_TR": "", "uk_UA": "Турборежим:", "zh_CN": "涡轮模式: ", @@ -23485,7 +23485,7 @@ "pt_BR": "Tecla de atalho do Modo Turbo.\nConfigure o comportamento do Modo Turbo nas configurações de CPU do Ryujinx.\n\nDeixe Não Atribuído se não tiver certeza.", "ru_RU": "", "sv_SE": "Snabbtangenten för turboläge.\nKonfigurera beteendet för turboläge i Ryujinx CPU-inställningar.\n\nLämna Obunden om du är osäker.", - "th_TH": "", + "th_TH": "ปุ่มลัดสำหรับโหมดเทอร์โบ กำหนดพฤติกรรมของโหมดเทอร์โบในการตั้งค่า CPU ของ Ryujinx\n\nปล่อยเป็น “ไม่จำกัด” (Unbound) หากไม่แน่ใจ", "tr_TR": "", "uk_UA": "Гаряча клавіша Турборежиму.\nКонфігурацію поведінки турборежиму можна знайти в пункті меню \"Процесор\" в налаштуваннях Ryujinx.\n\nЗалиште неприв'язаною, якщо не впевнені.", "zh_CN": "涡轮模式热键。\n可以在 Ryujinx CPU 设置中配置涡轮模式的行为。\n\n如果不确定请保留为未绑定。", @@ -23510,7 +23510,7 @@ "pt_BR": "Somente enquanto pressionado.", "ru_RU": "", "sv_SE": "Endast när du trycker ner", - "th_TH": "", + "th_TH": "ทำงานเฉพาะขณะกดปุ่ม", "tr_TR": "", "uk_UA": "Лише під час натискання", "zh_CN": "仅在按下时", @@ -23535,7 +23535,7 @@ "pt_BR": "Última atualização: {0}", "ru_RU": "Последнее обновление: {0}", "sv_SE": "Senast uppdaterad: {0}", - "th_TH": "", + "th_TH": "อัปเดตล่าสุด: {0}", "tr_TR": "", "uk_UA": "Останнє оновлення: {0}", "zh_CN": "最后更新于: {0}", @@ -23560,7 +23560,7 @@ "pt_BR": "Lista de Compatibilidade - {0} registros", "ru_RU": "Список совместимости — записей: {0}", "sv_SE": "Kompatibilitetslista - {0} poster", - "th_TH": "", + "th_TH": "รายการความเข้ากันได้ - {0} รายการ", "tr_TR": "", "uk_UA": "Список сумісності — {0} ігор", "zh_CN": "兼容性列表 - {0} 条", @@ -23585,7 +23585,7 @@ "pt_BR": "Esta lista de compatibilidade pode estar desatualizada.\nNão se oponha a testar os jogos", "ru_RU": "В списке совместимости могут содержаться устаревшие записи.\nНе стестняйтесь тестировать игр в статусе «Запускается»", "sv_SE": "Denna kompatibilitetslista kan innehålla utdaterade poster.\nTesta gärna spelen som listas med \"Spelproblem\"-status.", - "th_TH": "", + "th_TH": "รายการความเข้ากันได้นี้อาจมีข้อมูลที่ล้าสมัย\nโปรดอย่าลังเลที่จะทดสอบเกมที่มีสถานะเป็น \"Ingame\"", "tr_TR": "", "uk_UA": "Цей список сумісності може містити застарілі дані.\nНе відмовляйтеся від тестування ігор що мають статус \"Запускаються\".", "zh_CN": "此兼容性列表可能包含过时的条目。\n不要只测试 \"进入游戏\" 状态的游戏。", @@ -23610,7 +23610,7 @@ "pt_BR": "Pesquisa de compatibilidade", "ru_RU": "Поиск записей о совместимости...", "sv_SE": "Sök i kompatibilitetsposter...", - "th_TH": "", + "th_TH": "ค้นหารายการความเข้ากันได้...", "tr_TR": "", "uk_UA": "Перевірити сумісність гри...", "zh_CN": "正在搜索兼容性条目...", @@ -23635,7 +23635,7 @@ "pt_BR": "Procurando {0} registros de compatibilidade...", "ru_RU": "Поиск среди {0} записей о совместимости...", "sv_SE": "Sök i {0} kompatibilitetsposter...", - "th_TH": "", + "th_TH": "ค้นหารายการความเข้ากันได้ {0} รายการ...", "tr_TR": "", "uk_UA": "Шукати серед {0} перевірених ігор...", "zh_CN": "搜索 {0} 兼容性条目...", @@ -23660,7 +23660,7 @@ "pt_BR": "Lista de Compatibilidade", "ru_RU": "Открыть список совместимости", "sv_SE": "Öppna kompatibilitetslistan", - "th_TH": "", + "th_TH": "เปิดรายการความเข้ากันได้", "tr_TR": "", "uk_UA": "Відкрити Список Сумісності", "zh_CN": "打开兼容性列表", @@ -23685,7 +23685,7 @@ "pt_BR": "Jogos e Aplicativos", "ru_RU": "Игры и Приложения", "sv_SE": "Spel och applikationer", - "th_TH": "", + "th_TH": "เกมและแอปพลิเคชัน", "tr_TR": "Oyunlar ve Uygulamalar", "uk_UA": "Ігри та Додатки", "zh_CN": "游戏和应用程序", @@ -23735,7 +23735,7 @@ "pt_BR": "Problemas e Características", "ru_RU": "Проблемы и Особенности", "sv_SE": "Problem och egenskaper", - "th_TH": "", + "th_TH": "ข้อผิดพลาดและคุณสมบัติ", "tr_TR": "Sorunlar ve Özellikler", "uk_UA": "Проблеми та Особливості", "zh_CN": "问题和特性", @@ -23760,7 +23760,7 @@ "pt_BR": "", "ru_RU": "Инфо", "sv_SE": "", - "th_TH": "", + "th_TH": "รายละเอียด", "tr_TR": "Bilgi", "uk_UA": "Інфо", "zh_CN": "信息", @@ -23785,7 +23785,7 @@ "pt_BR": "Mostrar apenas jogos disponíveis", "ru_RU": "Показывать только свои игры", "sv_SE": "Visa endast ägda spel", - "th_TH": "", + "th_TH": "แสดงเฉพาะเกมที่เป็นเจ้าของ", "tr_TR": "", "uk_UA": "Показувати лише ігри в наявності", "zh_CN": "仅显示拥有的游戏", @@ -23810,7 +23810,7 @@ "pt_BR": "Jogável", "ru_RU": "Играбельно", "sv_SE": "Spelbart", - "th_TH": "", + "th_TH": "สามารถเล่นได้", "tr_TR": "", "uk_UA": "Справна", "zh_CN": "可游玩", @@ -23835,7 +23835,7 @@ "pt_BR": "No jogo", "ru_RU": "Запускается", "sv_SE": "Spelproblem", - "th_TH": "", + "th_TH": "กำลังอยู่ในเกม", "tr_TR": "", "uk_UA": "З недоліками", "zh_CN": "进入游戏", @@ -23860,7 +23860,7 @@ "pt_BR": "Menu", "ru_RU": "Меню", "sv_SE": "Menyer", - "th_TH": "", + "th_TH": "เมนู", "tr_TR": "", "uk_UA": "Меню", "zh_CN": "菜单", @@ -23885,7 +23885,7 @@ "pt_BR": "Inicializa", "ru_RU": "Стартует", "sv_SE": "Startar", - "th_TH": "", + "th_TH": "บูต", "tr_TR": "", "uk_UA": "Запускається", "zh_CN": "启动", @@ -23910,7 +23910,7 @@ "pt_BR": "Nada", "ru_RU": "Ничего", "sv_SE": "Ingenting", - "th_TH": "", + "th_TH": "ว่างเปล่า", "tr_TR": "", "uk_UA": "Не працює", "zh_CN": "什么都没有", @@ -23935,7 +23935,7 @@ "pt_BR": "Inicializa e roda sem travamentos ou bugs de GPU de qualquer tipo, e em uma velocidade rápida o suficiente para ser aproveitado em um PC comum.", "ru_RU": "Запускается и работает без любого рода сбоев или графисечких ошибок и на скорости, достаточной для работы на обычном ПК.", "sv_SE": "Startar upp och spelas utan några krascher eller GPU-fel av några slag och med en hastighet som är snabb nog för bra upplevelse på en genomsnittlig PC.", - "th_TH": "", + "th_TH": "สามารถบูตและเล่นได้โดยไม่มีการแครชหรือบั๊กของ GPU ใด ๆ และมีความเร็วเพียงพอที่จะเล่นได้อย่างสนุกสนานบนพีซีทั่วไป", "tr_TR": "", "uk_UA": "Запускається та оптимально працює (без збоїв або графічних багів) на середньостатистичному комп'ютері.", "zh_CN": "启动和游戏时不会出现任何崩溃或任何类型的 GPU bug 且速度足够快可以在一般 PC 上尽情游玩。", @@ -23960,7 +23960,7 @@ "pt_BR": "Inicializa e entra no jogo, mas sofre de um ou mais dos seguintes: travamentos, deadlocks, bugs de GPU, áudio ruim que distrai ou é simplesmente muito lento. O jogo ainda pode ser jogado até o fim, mas não da forma como foi criado para ser jogado.", "ru_RU": "Запускается и работает, но возникает одна или несколько из следующих проблем: сбои, взаимоблокировки, ошибки GPU, отвлекающие звуки или просто слишком медленная работа. Возможно, игру всё же удастся пройти до конца, но не так, как она задумана.", "sv_SE": "Startar och går in i spelet men lider av ett eller flera av följande: kraschar, deadlocks, GPU-buggar, distraherande dåligt ljud eller är helt enkelt för långsamt. Spelet kan fortfarande spelas hela vägen igenom, men inte så som spelet är avsett att spelas.", - "th_TH": "", + "th_TH": "สามารถบูตและเข้าเล่นเกมได้ แต่พบปัญหาหนึ่งหรือมากกว่านี้ เช่น แครช, ระบบหยุดทำงาน, บั๊กของ GPU, เสียงแย่จนรบกวน หรือช้าเกินไปเกมอาจยังเล่นจบได้ แต่ไม่สามารถเล่นได้ตามที่เกมตั้งใจไว้", "tr_TR": "", "uk_UA": "Запускається, але в грі на вас чекатимуть одна або декілька наступних проблем: збої, зависання, графічні баги, спотворений звук або ж гра загалом працюватиме надто повільно. Можливо, гру все ще можна буде пройти, але досвід буде не найкращим.", "zh_CN": "可以成功启动并进入游戏但可能会遇到以下一种或多种问题: 崩溃、卡死、GPU bug、令人无法接受的音频,或者只是太慢。仍然可以继续进行游戏,但是可能无法达到预期。", @@ -23985,7 +23985,7 @@ "pt_BR": "Inicializa e passa da tela de título, mas não entra no jogo principal.", "ru_RU": "Загружается титульный экран и можно перейти дальше, но сама игра не работает.", "sv_SE": "Startar upp och går förbi titelskärmen men tar sig inte in i huvudspelet.", - "th_TH": "", + "th_TH": "สามารถบูตและผ่านหน้าจอชื่อเกมได้ แต่ไม่สามารถเข้าสู่การเล่นเกมหลักได้", "tr_TR": "", "uk_UA": "Запускається та проходить початковий екран, проте зіграти у вас не вийде.", "zh_CN": "可以启动并通过标题画面但是无法进入到主要的游戏流程。", @@ -24010,7 +24010,7 @@ "pt_BR": "Inizializa, mas não passa da tela de título.", "ru_RU": "Загружается, но не проходит дальше титульного экрана.", "sv_SE": "Startar upp men tar sig inte förbi titelskärmen.", - "th_TH": "", + "th_TH": "บูตได้แต่ไม่ผ่านหน้าจอชื่อเกม", "tr_TR": "", "uk_UA": "Запускається, але не відображає навіть початкового екрану.", "zh_CN": "可以启动但是无法通过标题画面。", @@ -24035,7 +24035,7 @@ "pt_BR": "Não inicializa ou não mostra sinais de atividade.", "ru_RU": "Не запускается или не подаёт признаков жизни.", "sv_SE": "Startar inte upp eller visar någon form av aktivitet.", - "th_TH": "", + "th_TH": "ไม่สามารถบูตได้หรือไม่มีสัญญาณการทำงานใด ๆ", "tr_TR": "", "uk_UA": "Взагалі не запускається.", "zh_CN": "无法启动或显示无任何动静。", @@ -24060,7 +24060,7 @@ "pt_BR": "Configurações Customizadas", "ru_RU": "Индивидуальные параметры", "sv_SE": "Anpassad konfiguration", - "th_TH": "", + "th_TH": "การตั้งค่าแบบกำหนดเอง", "tr_TR": "", "uk_UA": "Власна конфігурація", "zh_CN": "自定义配置", @@ -24085,7 +24085,7 @@ "pt_BR": "", "ru_RU": "(Глобальный)", "sv_SE": "", - "th_TH": "", + "th_TH": "(ทั่วทั้งระบบ)", "tr_TR": "", "uk_UA": "(Глобальні)", "zh_CN": "(全局)", @@ -24110,7 +24110,7 @@ "pt_BR": "Selecione um DLC para Extrair", "ru_RU": "Выберите DLC для извлечения", "sv_SE": "Välj en DLC att extrahera", - "th_TH": "", + "th_TH": "เลือก DLC สำหรับการแยกไฟล์", "tr_TR": "", "uk_UA": "Оберіть DLC які бажаєте вилучити", "zh_CN": "选择一个要解压的 DLC", @@ -24135,7 +24135,7 @@ "pt_BR": "Imagem da Presença do Discord", "ru_RU": "Изображение для статуса активности", "sv_SE": "Bild för Rich Presence", - "th_TH": "", + "th_TH": "ภาพสถานะ Rich Presence", "tr_TR": "", "uk_UA": "Зображення картки активності Discord", "zh_CN": "Rich Presence 图像", @@ -24160,7 +24160,7 @@ "pt_BR": "Presença Dinâmica do Discord", "ru_RU": "Динамический статус активности", "sv_SE": "Dynamisk Rich Presence", - "th_TH": "", + "th_TH": "สถานะ Rich Presence แบบไดนามิก", "tr_TR": "", "uk_UA": "Динамічна картка активності Discord", "zh_CN": "动态 Rich Presence", From c482b7a1c00f8190d28c8dcc6218d1f77733c1a4 Mon Sep 17 00:00:00 2001 From: LotP <22-lotp@users.noreply.git.ryujinx.app> Date: Sat, 9 Aug 2025 17:46:29 -0500 Subject: [PATCH 09/24] Fix crash caused by VirtualRange mismatch (ryubing/ryujinx!109) See merge request ryubing/ryujinx!109 --- src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs b/src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs index 7803b03d1..e7070b031 100644 --- a/src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs +++ b/src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs @@ -157,7 +157,7 @@ namespace Ryujinx.Memory.Range { int index = BinarySearch(item.Address); - if (index >= 0 && Items[index].Value.Equals(item)) + if (index >= 0) { _quickAccess.Remove(item.Address); From d6d089b81b14d760bf2afcd5ddaf551d146483c7 Mon Sep 17 00:00:00 2001 From: LotP <22-lotp@users.noreply.git.ryujinx.app> Date: Sat, 9 Aug 2025 18:41:36 -0500 Subject: [PATCH 10/24] Revert "Fix crash caused by VirtualRange mismatch (ryubing/ryujinx!109)" (ryubing/ryujinx!110) See merge request ryubing/ryujinx!110 --- src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs b/src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs index e7070b031..7803b03d1 100644 --- a/src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs +++ b/src/Ryujinx.Memory/Range/NonOverlappingRangeList.cs @@ -157,7 +157,7 @@ namespace Ryujinx.Memory.Range { int index = BinarySearch(item.Address); - if (index >= 0) + if (index >= 0 && Items[index].Value.Equals(item)) { _quickAccess.Remove(item.Address); From c863ffd35318ab2b2c286dda000fc75178f54195 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hack=E8=8C=B6=E3=82=93?= Date: Sun, 10 Aug 2025 16:37:14 -0500 Subject: [PATCH 11/24] Update Korean translation (ryubing/ryujinx!107) See merge request ryubing/ryujinx!107 --- assets/locales.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/assets/locales.json b/assets/locales.json index 3ac0035a5..a5f216057 100644 --- a/assets/locales.json +++ b/assets/locales.json @@ -24179,7 +24179,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "디버그", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -24204,7 +24204,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "디버그", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -24229,7 +24229,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "경고 : 개발자 전용으로, 성능이 저하될 수 있습니다.", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -24254,7 +24254,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "GDB Stub 활성화", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -24279,7 +24279,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "GDB Stub을 활성화하여 실행 중인 응용 프로그램을 디버그할 수 있도록 합니다. 개발 용도로만 사용하십시오!", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -24304,7 +24304,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "GDB Stub 포트 :", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -24329,7 +24329,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "시작 시, 앱 일시 중지", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -24354,7 +24354,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "1번째 명령어를 실행하기 전에 앱앱을 일시 중지하여 가장 초기의 단계에서 디버깅을 가능하게 합니다.", "no_NO": "", "pl_PL": "", "pt_BR": "", From dd5e1b99b19d980f36d6ae4f61e32d68019273b5 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Mon, 11 Aug 2025 18:00:10 -0500 Subject: [PATCH 12/24] remove localization entries for auto graphics backend --- assets/locales.json | 52 +-------------------------------------------- 1 file changed, 1 insertion(+), 51 deletions(-) diff --git a/assets/locales.json b/assets/locales.json index a5f216057..5fa490458 100644 --- a/assets/locales.json +++ b/assets/locales.json @@ -21342,56 +21342,6 @@ "zh_TW": "選擇模擬器將使用的圖形後端。\n\n只要驅動程式是最新的,Vulkan 對所有現代顯示卡來說都更好用。Vulkan 還能在所有 GPU 廠商上實現更快的著色器編譯 (減少卡頓)。\n\nOpenGL 在舊式 Nvidia GPU、Linux 上的舊式 AMD GPU 或 VRAM 較低的 GPU 上可能會取得更好的效果,不過著色器編譯的卡頓會更嚴重。\n\n如果不確定,請設定為 Vulkan。如果您的 GPU 使用最新的圖形驅動程式也不支援 Vulkan,請設定為 OpenGL。" } }, - { - "ID": "SettingsTabGraphicsBackendAuto", - "Translations": { - "ar_SA": "", - "de_DE": "", - "el_GR": "", - "en_US": "Auto", - "es_ES": "", - "fr_FR": "", - "he_IL": "", - "it_IT": "Automatico", - "ja_JP": "", - "ko_KR": "자동", - "no_NO": "", - "pl_PL": "", - "pt_BR": "Automático", - "ru_RU": "Авто", - "sv_SE": "Automatiskt", - "th_TH": "อัตโนมัติ", - "tr_TR": "", - "uk_UA": "Автоматично", - "zh_CN": "自动", - "zh_TW": "自動" - } - }, - { - "ID": "SettingsTabGraphicsBackendAutoTooltip", - "Translations": { - "ar_SA": "", - "de_DE": "", - "el_GR": "", - "en_US": "Uses Vulkan.\nOn an ARM Mac, and when playing a game that runs well under it, uses the Metal backend.", - "es_ES": "", - "fr_FR": "Utilise Vulkan.\nSur un Mac ARM, et lorsqu’un jeu fonctionne bien avec, utilise le moteur Metal.", - "he_IL": "", - "it_IT": "Utilizza Vulkan.\nSu un Mac con processore ARM, utilizza il backend Metal nei giochi che funzionano bene con quest'ultimo.", - "ja_JP": "", - "ko_KR": "Vulkan을 사용합니다.\nARM 맥에서 해당 플랫폼에서 잘 실행되는 게임을 플레이하는 경우 Metal 후단부를 사용합니다.", - "no_NO": "Bruker Vulkan \nPå en ARM Mac, og når du spiller et spill som kjører bra under den, bruker du Metal-backend.", - "pl_PL": "", - "pt_BR": "Usa Vulkan.\nEm um Mac ARM, e ao jogar um jogo que roda bem nele, usa o renderizador Metal.", - "ru_RU": "Использует Vulkan.\nНа Mac с ARM процессорами используется Metal, если игра с ним совместима и хорошо работает.", - "sv_SE": "Använder Vulkan.\nPå en ARM Mac och vid spel som körs bra på den så används Metal-bakänden.", - "th_TH": "ใช้ Vulkan\nบน Mac (ARM) จะใช้ Metal หากเกมทำงานได้ดีกว่า", - "tr_TR": "", - "uk_UA": "Використовує Vulkan.\nНа Mac з процесором ARM використовується графічний API Metal, якщо гра під нього оптимізована.", - "zh_CN": "使用 Vulkan。\n在 ARM Mac 上,当玩在其下运行良好的游戏时,使用 Metal 后端。", - "zh_TW": "使用Vulkan。\n在 ARM Mac 上,如果遊戲執行性能良好時,則將使用 Metal 後端。" - } - }, { "ID": "SettingsEnableTextureRecompression", "Translations": { @@ -24368,4 +24318,4 @@ } } ] -} \ No newline at end of file +} From c6f22318a7f1a259381ff622e68a5fdda43579b8 Mon Sep 17 00:00:00 2001 From: GreemDev Date: Mon, 11 Aug 2025 18:06:53 -0500 Subject: [PATCH 13/24] add an ASCII header at startup in the log --- src/Ryujinx/Program.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index 09759d7cc..db9ae21eb 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -301,6 +301,13 @@ namespace Ryujinx.Ava internal static void PrintSystemInfo() { + 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, $"{RyujinxApp.FullAppName} Version: {Version}"); Logger.Notice.Print(LogClass.Application, $".NET Runtime: {RuntimeInformation.FrameworkDescription}"); SystemInfo.Gather().Print(); From ae2e9a73ab1175c0326b6d8f23425f9356ab8bd6 Mon Sep 17 00:00:00 2001 From: Neo Date: Tue, 12 Aug 2025 17:45:24 -0500 Subject: [PATCH 14/24] UI Updates Batch 2 (ryubing/ryujinx!105) See merge request ryubing/ryujinx!105 --- assets/locales.json | 30 +++++++++---------- src/Ryujinx/Assets/Styles/Styles.xaml | 4 +-- src/Ryujinx/Systems/AppHost.cs | 15 ++++++++-- .../Converters/PlayabilityStatusConverter.cs | 10 +++---- .../UI/Views/Main/MainViewControls.axaml | 8 ++--- .../Views/Settings/SettingsHotkeysView.axaml | 5 ++-- .../UI/Windows/CompatibilityListWindow.axaml | 20 ++++++------- src/Ryujinx/UI/Windows/SettingsWindow.axaml | 5 ++-- 8 files changed, 53 insertions(+), 44 deletions(-) diff --git a/assets/locales.json b/assets/locales.json index 5fa490458..8b449c686 100644 --- a/assets/locales.json +++ b/assets/locales.json @@ -23693,28 +23693,28 @@ } }, { - "ID": "CompatibilityListInfo", + "ID": "CompatibilityListStats", "Translations": { "ar_SA": "", "de_DE": "", - "el_GR": "Πληροφορίες", - "en_US": "Info", - "es_ES": "Información", - "fr_FR": "", - "he_IL": "מידע", + "el_GR": "", + "en_US": "Stats", + "es_ES": null, + "fr_FR": null, + "he_IL": "", "it_IT": "", - "ja_JP": "情報", - "ko_KR": "정보", + "ja_JP": "", + "ko_KR": "", "no_NO": "", - "pl_PL": "Informacja", + "pl_PL": "", "pt_BR": "", - "ru_RU": "Инфо", + "ru_RU": "Данные", "sv_SE": "", - "th_TH": "รายละเอียด", - "tr_TR": "Bilgi", - "uk_UA": "Інфо", - "zh_CN": "信息", - "zh_TW": "資訊" + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" } }, { diff --git a/src/Ryujinx/Assets/Styles/Styles.xaml b/src/Ryujinx/Assets/Styles/Styles.xaml index ea6096792..34c7e2516 100644 --- a/src/Ryujinx/Assets/Styles/Styles.xaml +++ b/src/Ryujinx/Assets/Styles/Styles.xaml @@ -266,7 +266,7 @@