From 07690e452726d64054dca239fd3e0b0a6e333287 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Wed, 4 Dec 2024 02:24:40 -0600 Subject: [PATCH 01/79] chore: applets: Cleanup redundant ReadStruct implementations & provide a default implementation for IApplet#GetResult. --- src/Ryujinx.HLE/HOS/Applets/AppletManager.cs | 4 +--- .../HOS/Applets/Browser/BrowserApplet.cs | 7 ------- .../HOS/Applets/Cabinet/CabinetApplet.cs | 13 ------------- .../HOS/Applets/Controller/ControllerApplet.cs | 5 ----- src/Ryujinx.HLE/HOS/Applets/Dummy/DummyApplet.cs | 12 ++++-------- src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs | 5 ----- src/Ryujinx.HLE/HOS/Applets/IApplet.cs | 2 +- .../HOS/Applets/PlayerSelect/PlayerSelectApplet.cs | 5 ----- .../SoftwareKeyboard/SoftwareKeyboardApplet.cs | 5 ----- .../HOS/Applets/SoftwareKeyboard/TRef.cs | 2 +- 10 files changed, 7 insertions(+), 53 deletions(-) diff --git a/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs b/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs index a2ddd573d..5895c67bb 100644 --- a/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs +++ b/src/Ryujinx.HLE/HOS/Applets/AppletManager.cs @@ -24,11 +24,9 @@ namespace Ryujinx.HLE.HOS.Applets case AppletId.SoftwareKeyboard: return new SoftwareKeyboardApplet(system); case AppletId.LibAppletWeb: - return new BrowserApplet(system); case AppletId.LibAppletShop: - return new BrowserApplet(system); case AppletId.LibAppletOff: - return new BrowserApplet(system); + return new BrowserApplet(); case AppletId.MiiEdit: Logger.Warning?.Print(LogClass.Application, $"Please use the MiiEdit inside File/Open Applet"); return new DummyApplet(system); diff --git a/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserApplet.cs b/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserApplet.cs index 6afbe4a72..c5f13dab3 100644 --- a/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserApplet.cs +++ b/src/Ryujinx.HLE/HOS/Applets/Browser/BrowserApplet.cs @@ -18,13 +18,6 @@ namespace Ryujinx.HLE.HOS.Applets.Browser private List _arguments; private ShimKind _shimKind; - public BrowserApplet(Horizon system) { } - - public ResultCode GetResult() - { - return ResultCode.Success; - } - public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession) { _normalSession = normalSession; diff --git a/src/Ryujinx.HLE/HOS/Applets/Cabinet/CabinetApplet.cs b/src/Ryujinx.HLE/HOS/Applets/Cabinet/CabinetApplet.cs index f4f935d34..294b8d1f6 100644 --- a/src/Ryujinx.HLE/HOS/Applets/Cabinet/CabinetApplet.cs +++ b/src/Ryujinx.HLE/HOS/Applets/Cabinet/CabinetApplet.cs @@ -125,19 +125,6 @@ namespace Ryujinx.HLE.HOS.Applets.Cabinet return bytes; } - public static T ReadStruct(byte[] data) where T : unmanaged - { - if (data.Length < Unsafe.SizeOf()) - { - throw new ArgumentException("Not enough data to read the struct"); - } - - fixed (byte* dataPtr = data) - { - return Unsafe.Read(dataPtr); - } - } - #region Structs [StructLayout(LayoutKind.Sequential, Pack = 1)] diff --git a/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerApplet.cs b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerApplet.cs index 5ec9d4b08..3a7b29ab5 100644 --- a/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerApplet.cs +++ b/src/Ryujinx.HLE/HOS/Applets/Controller/ControllerApplet.cs @@ -117,11 +117,6 @@ namespace Ryujinx.HLE.HOS.Applets return ResultCode.Success; } - public ResultCode GetResult() - { - return ResultCode.Success; - } - private static byte[] BuildResponse(ControllerSupportResultInfo result) { using MemoryStream stream = MemoryStreamManager.Shared.GetStream(); diff --git a/src/Ryujinx.HLE/HOS/Applets/Dummy/DummyApplet.cs b/src/Ryujinx.HLE/HOS/Applets/Dummy/DummyApplet.cs index 75df7a373..6b16aee7b 100644 --- a/src/Ryujinx.HLE/HOS/Applets/Dummy/DummyApplet.cs +++ b/src/Ryujinx.HLE/HOS/Applets/Dummy/DummyApplet.cs @@ -11,11 +11,14 @@ namespace Ryujinx.HLE.HOS.Applets.Dummy { private readonly Horizon _system; private AppletSession _normalSession; + public event EventHandler AppletStateChanged; + public DummyApplet(Horizon system) { _system = system; } + public ResultCode Start(AppletSession normalSession, AppletSession interactiveSession) { _normalSession = normalSession; @@ -24,10 +27,7 @@ namespace Ryujinx.HLE.HOS.Applets.Dummy _system.ReturnFocus(); return ResultCode.Success; } - private static T ReadStruct(byte[] data) where T : struct - { - return MemoryMarshal.Read(data.AsSpan()); - } + private static byte[] BuildResponse() { using MemoryStream stream = MemoryStreamManager.Shared.GetStream(); @@ -35,9 +35,5 @@ namespace Ryujinx.HLE.HOS.Applets.Dummy writer.Write((ulong)ResultCode.Success); return stream.ToArray(); } - public ResultCode GetResult() - { - return ResultCode.Success; - } } } diff --git a/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs index 87d88fc65..0e043cc45 100644 --- a/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs +++ b/src/Ryujinx.HLE/HOS/Applets/Error/ErrorApplet.cs @@ -203,10 +203,5 @@ namespace Ryujinx.HLE.HOS.Applets.Error _horizon.Device.UIHandler.DisplayErrorAppletDialog($"Error Number: {applicationErrorArg.ErrorNumber} (Details)", "\n" + detailsText, buttons.ToArray()); } } - - public ResultCode GetResult() - { - return ResultCode.Success; - } } } diff --git a/src/Ryujinx.HLE/HOS/Applets/IApplet.cs b/src/Ryujinx.HLE/HOS/Applets/IApplet.cs index bc5353841..4500b2f63 100644 --- a/src/Ryujinx.HLE/HOS/Applets/IApplet.cs +++ b/src/Ryujinx.HLE/HOS/Applets/IApplet.cs @@ -13,7 +13,7 @@ namespace Ryujinx.HLE.HOS.Applets ResultCode Start(AppletSession normalSession, AppletSession interactiveSession); - ResultCode GetResult(); + ResultCode GetResult() => ResultCode.Success; bool DrawTo(RenderingSurfaceInfo surfaceInfo, IVirtualMemoryManager destination, ulong position) => false; diff --git a/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs b/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs index ccc761ba1..05bddc76f 100644 --- a/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs +++ b/src/Ryujinx.HLE/HOS/Applets/PlayerSelect/PlayerSelectApplet.cs @@ -37,11 +37,6 @@ namespace Ryujinx.HLE.HOS.Applets return ResultCode.Success; } - public ResultCode GetResult() - { - return ResultCode.Success; - } - private byte[] BuildResponse() { UserProfile currentUser = _system.AccountManager.LastOpenedUser; diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs index e04fc64fe..9ec202357 100644 --- a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs @@ -144,11 +144,6 @@ namespace Ryujinx.HLE.HOS.Applets } } - public ResultCode GetResult() - { - return ResultCode.Success; - } - private bool IsKeyboardActive() { return _backgroundState >= InlineKeyboardState.Appearing && _backgroundState < InlineKeyboardState.Disappearing; diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/TRef.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/TRef.cs index 32d9e68da..51571401f 100644 --- a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/TRef.cs +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/TRef.cs @@ -2,7 +2,7 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { /// /// Wraps a type in a class so it gets stored in the GC managed heap. This is used as communication mechanism - /// between classed that need to be disposed and, thus, can't share their references. + /// between classes that need to be disposed and, thus, can't share their references. /// /// The internal type. class TRef From 1d0152b9617a8918c7db3d01873bbf46c546c969 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Wed, 4 Dec 2024 03:37:21 -0600 Subject: [PATCH 02/79] UI: Move Shader Compilation hint, graphics backend, and GPU manufacturer to the right side of the status bar, next to firmware version. Removed the "Game:" prefix in front of FPS. --- Directory.Packages.props | 2 +- .../LdnRyu/Proxy/P2pProxyServer.cs | 12 ++++- src/Ryujinx/AppHost.cs | 2 +- src/Ryujinx/Assets/Locales/ar_SA.json | 1 - src/Ryujinx/Assets/Locales/de_DE.json | 1 - src/Ryujinx/Assets/Locales/el_GR.json | 1 - src/Ryujinx/Assets/Locales/en_US.json | 1 - src/Ryujinx/Assets/Locales/es_ES.json | 1 - src/Ryujinx/Assets/Locales/fr_FR.json | 1 - src/Ryujinx/Assets/Locales/he_IL.json | 1 - src/Ryujinx/Assets/Locales/it_IT.json | 1 - src/Ryujinx/Assets/Locales/ja_JP.json | 1 - src/Ryujinx/Assets/Locales/ko_KR.json | 1 - src/Ryujinx/Assets/Locales/pl_PL.json | 1 - src/Ryujinx/Assets/Locales/pt_BR.json | 1 - src/Ryujinx/Assets/Locales/ru_RU.json | 1 - src/Ryujinx/Assets/Locales/th_TH.json | 1 - src/Ryujinx/Assets/Locales/tr_TR.json | 1 - src/Ryujinx/Assets/Locales/uk_UA.json | 1 - src/Ryujinx/Assets/Locales/zh_CN.json | 1 - src/Ryujinx/Assets/Locales/zh_TW.json | 1 - .../UI/ViewModels/MainWindowViewModel.cs | 10 ++--- .../UI/Views/Main/MainMenuBarView.axaml.cs | 3 +- .../UI/Views/Main/MainStatusBarView.axaml | 45 +++++++++++-------- src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 7 ++- src/Ryujinx/Updater.cs | 11 +++-- 26 files changed, 52 insertions(+), 58 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index ffb5f2ead..7059af0e0 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -38,7 +38,7 @@ - + diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs index 598fb654f..fbce5c10c 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/LdnRyu/Proxy/P2pProxyServer.cs @@ -1,3 +1,5 @@ +using Gommon; +using Humanizer; using NetCoreServer; using Open.Nat; using Ryujinx.Common.Logging; @@ -153,7 +155,10 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy if (_publicPort != 0) { - _ = Task.Delay(PortLeaseRenew * 1000, _disposedCancellation.Token).ContinueWith((task) => Task.Run(RefreshLease)); + _ = Executor.ExecuteAfterDelayAsync( + PortLeaseRenew.Seconds(), + _disposedCancellation.Token, + RefreshLease); } _natDevice = device; @@ -257,7 +262,10 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator.LdnRyu.Proxy } - _ = Task.Delay(PortLeaseRenew, _disposedCancellation.Token).ContinueWith((task) => Task.Run(RefreshLease)); + _ = Executor.ExecuteAfterDelayAsync( + PortLeaseRenew.Milliseconds(), + _disposedCancellation.Token, + RefreshLease); } public bool TryRegisterUser(P2pProxySession session, ExternalProxyConfig config) diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index 5789737d6..9a7f82661 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -1137,7 +1137,7 @@ namespace Ryujinx.Ava LocaleManager.Instance[LocaleKeys.VolumeShort] + $": {(int)(Device.GetVolume() * 100)}%", dockedMode, ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(), - LocaleManager.Instance[LocaleKeys.Game] + $": {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)", + $"{Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)", $"FIFO: {Device.Statistics.GetFifoPercent():00.00} %", _displayCount)); } diff --git a/src/Ryujinx/Assets/Locales/ar_SA.json b/src/Ryujinx/Assets/Locales/ar_SA.json index c1ee30f19..412695af6 100644 --- a/src/Ryujinx/Assets/Locales/ar_SA.json +++ b/src/Ryujinx/Assets/Locales/ar_SA.json @@ -718,7 +718,6 @@ "UpdaterAddingFiles": "إضافة ملفات جديدة...", "UpdaterExtracting": "استخراج التحديث...", "UpdaterDownloading": "تحميل التحديث...", - "Game": "لعبة", "Docked": "تركيب بالمنصة", "Handheld": "محمول", "ConnectionError": "خطأ في الاتصال", diff --git a/src/Ryujinx/Assets/Locales/de_DE.json b/src/Ryujinx/Assets/Locales/de_DE.json index e3f6b1be1..76e8dfadd 100644 --- a/src/Ryujinx/Assets/Locales/de_DE.json +++ b/src/Ryujinx/Assets/Locales/de_DE.json @@ -718,7 +718,6 @@ "UpdaterAddingFiles": "Neue Dateien hinzufügen...", "UpdaterExtracting": "Update extrahieren...", "UpdaterDownloading": "Update herunterladen...", - "Game": "Spiel", "Docked": "Docked", "Handheld": "Handheld", "ConnectionError": "Verbindungsfehler.", diff --git a/src/Ryujinx/Assets/Locales/el_GR.json b/src/Ryujinx/Assets/Locales/el_GR.json index e93e9310a..0409297ac 100644 --- a/src/Ryujinx/Assets/Locales/el_GR.json +++ b/src/Ryujinx/Assets/Locales/el_GR.json @@ -718,7 +718,6 @@ "UpdaterAddingFiles": "Προσθήκη Νέων Αρχείων...", "UpdaterExtracting": "Εξαγωγή Ενημέρωσης...", "UpdaterDownloading": "Λήψη Ενημέρωσης...", - "Game": "Παιχνίδι", "Docked": "Προσκολλημένο", "Handheld": "Χειροκίνητο", "ConnectionError": "Σφάλμα Σύνδεσης.", diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index ee0d03171..ba183c8bd 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -730,7 +730,6 @@ "UpdaterAddingFiles": "Adding New Files...", "UpdaterExtracting": "Extracting Update...", "UpdaterDownloading": "Downloading Update...", - "Game": "Game", "Docked": "Docked", "Handheld": "Handheld", "ConnectionError": "Connection Error.", diff --git a/src/Ryujinx/Assets/Locales/es_ES.json b/src/Ryujinx/Assets/Locales/es_ES.json index 0a68d44c6..b473b1197 100644 --- a/src/Ryujinx/Assets/Locales/es_ES.json +++ b/src/Ryujinx/Assets/Locales/es_ES.json @@ -718,7 +718,6 @@ "UpdaterAddingFiles": "Añadiendo nuevos archivos...", "UpdaterExtracting": "Extrayendo actualización...", "UpdaterDownloading": "Descargando actualización...", - "Game": "Juego", "Docked": "Dock/TV", "Handheld": "Portátil", "ConnectionError": "Error de conexión.", diff --git a/src/Ryujinx/Assets/Locales/fr_FR.json b/src/Ryujinx/Assets/Locales/fr_FR.json index 471dfbe5e..0223e322e 100644 --- a/src/Ryujinx/Assets/Locales/fr_FR.json +++ b/src/Ryujinx/Assets/Locales/fr_FR.json @@ -718,7 +718,6 @@ "UpdaterAddingFiles": "Ajout des nouveaux fichiers...", "UpdaterExtracting": "Extraction de la mise à jour…", "UpdaterDownloading": "Téléchargement de la mise à jour...", - "Game": "Jeu", "Docked": "Mode station d'accueil", "Handheld": "Mode Portable", "ConnectionError": "Erreur de connexion.", diff --git a/src/Ryujinx/Assets/Locales/he_IL.json b/src/Ryujinx/Assets/Locales/he_IL.json index dbacf5ea1..318068bf3 100644 --- a/src/Ryujinx/Assets/Locales/he_IL.json +++ b/src/Ryujinx/Assets/Locales/he_IL.json @@ -718,7 +718,6 @@ "UpdaterAddingFiles": "מוסיף קבצים חדשים...", "UpdaterExtracting": "מחלץ עדכון...", "UpdaterDownloading": "מוריד עדכון...", - "Game": "משחק", "Docked": "בתחנת עגינה", "Handheld": "נייד", "ConnectionError": "שגיאת חיבור", diff --git a/src/Ryujinx/Assets/Locales/it_IT.json b/src/Ryujinx/Assets/Locales/it_IT.json index 61ea2a355..5ca17bc2e 100644 --- a/src/Ryujinx/Assets/Locales/it_IT.json +++ b/src/Ryujinx/Assets/Locales/it_IT.json @@ -718,7 +718,6 @@ "UpdaterAddingFiles": "Aggiunta dei nuovi file...", "UpdaterExtracting": "Estrazione dell'aggiornamento...", "UpdaterDownloading": "Download dell'aggiornamento...", - "Game": "Gioco", "Docked": "TV", "Handheld": "Portatile", "ConnectionError": "Errore di connessione.", diff --git a/src/Ryujinx/Assets/Locales/ja_JP.json b/src/Ryujinx/Assets/Locales/ja_JP.json index 9acd1c486..ffa768c13 100644 --- a/src/Ryujinx/Assets/Locales/ja_JP.json +++ b/src/Ryujinx/Assets/Locales/ja_JP.json @@ -718,7 +718,6 @@ "UpdaterAddingFiles": "新規ファイルを追加中...", "UpdaterExtracting": "アップデートを展開中...", "UpdaterDownloading": "アップデートをダウンロード中...", - "Game": "ゲーム", "Docked": "ドッキング", "Handheld": "携帯", "ConnectionError": "接続エラー.", diff --git a/src/Ryujinx/Assets/Locales/ko_KR.json b/src/Ryujinx/Assets/Locales/ko_KR.json index 86592aa69..6b7140b3c 100644 --- a/src/Ryujinx/Assets/Locales/ko_KR.json +++ b/src/Ryujinx/Assets/Locales/ko_KR.json @@ -718,7 +718,6 @@ "UpdaterAddingFiles": "새 파일 추가...", "UpdaterExtracting": "업데이트 추출...", "UpdaterDownloading": "업데이트 내려받기 중...", - "Game": "게임", "Docked": "도킹", "Handheld": "휴대", "ConnectionError": "연결 오류가 발생했습니다.", diff --git a/src/Ryujinx/Assets/Locales/pl_PL.json b/src/Ryujinx/Assets/Locales/pl_PL.json index 1ed0988f9..d87453ef2 100644 --- a/src/Ryujinx/Assets/Locales/pl_PL.json +++ b/src/Ryujinx/Assets/Locales/pl_PL.json @@ -718,7 +718,6 @@ "UpdaterAddingFiles": "Dodawanie Nowych Plików...", "UpdaterExtracting": "Wypakowywanie Aktualizacji...", "UpdaterDownloading": "Pobieranie Aktualizacji...", - "Game": "Gra", "Docked": "Zadokowany", "Handheld": "Przenośny", "ConnectionError": "Błąd Połączenia.", diff --git a/src/Ryujinx/Assets/Locales/pt_BR.json b/src/Ryujinx/Assets/Locales/pt_BR.json index 676d89d96..c240bd804 100644 --- a/src/Ryujinx/Assets/Locales/pt_BR.json +++ b/src/Ryujinx/Assets/Locales/pt_BR.json @@ -717,7 +717,6 @@ "UpdaterAddingFiles": "Adicionando novos arquivos...", "UpdaterExtracting": "Extraíndo atualização...", "UpdaterDownloading": "Baixando atualização...", - "Game": "Jogo", "Docked": "TV", "Handheld": "Portátil", "ConnectionError": "Erro de conexão.", diff --git a/src/Ryujinx/Assets/Locales/ru_RU.json b/src/Ryujinx/Assets/Locales/ru_RU.json index ea4dcc8c8..1046208fb 100644 --- a/src/Ryujinx/Assets/Locales/ru_RU.json +++ b/src/Ryujinx/Assets/Locales/ru_RU.json @@ -718,7 +718,6 @@ "UpdaterAddingFiles": "Добавление новых файлов...", "UpdaterExtracting": "Извлечение обновления...", "UpdaterDownloading": "Загрузка обновления...", - "Game": "Игра", "Docked": "Стационарный режим", "Handheld": "Портативный режим", "ConnectionError": "Ошибка соединения", diff --git a/src/Ryujinx/Assets/Locales/th_TH.json b/src/Ryujinx/Assets/Locales/th_TH.json index fa4c1d334..e29004e10 100644 --- a/src/Ryujinx/Assets/Locales/th_TH.json +++ b/src/Ryujinx/Assets/Locales/th_TH.json @@ -718,7 +718,6 @@ "UpdaterAddingFiles": "กำลังเพิ่มไฟล์ใหม่...", "UpdaterExtracting": "กำลังแยกการอัปเดต...", "UpdaterDownloading": "กำลังดาวน์โหลดอัปเดต...", - "Game": "เกมส์", "Docked": "ด็อก", "Handheld": "แฮนด์เฮลด์", "ConnectionError": "การเชื่อมต่อล้มเหลว", diff --git a/src/Ryujinx/Assets/Locales/tr_TR.json b/src/Ryujinx/Assets/Locales/tr_TR.json index 475086e44..101206210 100644 --- a/src/Ryujinx/Assets/Locales/tr_TR.json +++ b/src/Ryujinx/Assets/Locales/tr_TR.json @@ -718,7 +718,6 @@ "UpdaterAddingFiles": "Yeni Dosyalar Ekleniyor...", "UpdaterExtracting": "Güncelleme Ayrıştırılıyor...", "UpdaterDownloading": "Güncelleme İndiriliyor...", - "Game": "Oyun", "Docked": "Docked", "Handheld": "El tipi", "ConnectionError": "Bağlantı Hatası.", diff --git a/src/Ryujinx/Assets/Locales/uk_UA.json b/src/Ryujinx/Assets/Locales/uk_UA.json index 68679a9b2..89e565bf3 100644 --- a/src/Ryujinx/Assets/Locales/uk_UA.json +++ b/src/Ryujinx/Assets/Locales/uk_UA.json @@ -718,7 +718,6 @@ "UpdaterAddingFiles": "Додавання нових файлів...", "UpdaterExtracting": "Видобування оновлення...", "UpdaterDownloading": "Завантаження оновлення...", - "Game": "Гра", "Docked": "Док-станція", "Handheld": "Портативний", "ConnectionError": "Помилка з'єднання.", diff --git a/src/Ryujinx/Assets/Locales/zh_CN.json b/src/Ryujinx/Assets/Locales/zh_CN.json index 741b5b370..66ac309de 100644 --- a/src/Ryujinx/Assets/Locales/zh_CN.json +++ b/src/Ryujinx/Assets/Locales/zh_CN.json @@ -718,7 +718,6 @@ "UpdaterAddingFiles": "安装更新中...", "UpdaterExtracting": "正在提取更新...", "UpdaterDownloading": "下载更新中...", - "Game": "游戏", "Docked": "主机模式", "Handheld": "掌机模式", "ConnectionError": "连接错误。", diff --git a/src/Ryujinx/Assets/Locales/zh_TW.json b/src/Ryujinx/Assets/Locales/zh_TW.json index aaf8170c0..792ced42b 100644 --- a/src/Ryujinx/Assets/Locales/zh_TW.json +++ b/src/Ryujinx/Assets/Locales/zh_TW.json @@ -718,7 +718,6 @@ "UpdaterAddingFiles": "正在加入新檔案...", "UpdaterExtracting": "正在提取更新...", "UpdaterDownloading": "正在下載更新...", - "Game": "遊戲", "Docked": "底座模式", "Handheld": "手提模式", "ConnectionError": "連線錯誤。", diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 3672f8c71..1bfcd439b 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -70,7 +70,7 @@ namespace Ryujinx.Ava.UI.ViewModels private string _gpuStatusText; private string _shaderCountText; private bool _isAmiiboRequested; - private bool _showRightmostSeparator; + private bool _showShaderCompilationHint; private bool _isGameRunning; private bool _isFullScreen; private int _progressMaximum; @@ -275,12 +275,12 @@ namespace Ryujinx.Ava.UI.ViewModels public bool ShowFirmwareStatus => !ShowLoadProgress; - public bool ShowRightmostSeparator + public bool ShowShaderCompilationHint { - get => _showRightmostSeparator; + get => _showShaderCompilationHint; set { - _showRightmostSeparator = value; + _showShaderCompilationHint = value; OnPropertyChanged(); } @@ -1497,7 +1497,7 @@ namespace Ryujinx.Ava.UI.ViewModels VolumeStatusText = args.VolumeStatus; FifoStatusText = args.FifoStatus; - ShaderCountText = (ShowRightmostSeparator = args.ShaderCount > 0) + ShaderCountText = (ShowShaderCompilationHint = args.ShaderCount > 0) ? $"{LocaleManager.Instance[LocaleKeys.CompilingShaders]}: {args.ShaderCount}" : string.Empty; diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs index 41b27e9c1..a3aa58f2c 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -200,7 +200,6 @@ namespace Ryujinx.Ava.UI.Views.Main await Dispatcher.UIThread.InvokeAsync(() => { - ViewModel.WindowState = WindowState.Normal; Window.Arrange(new Rect(Window.Position.X, Window.Position.Y, windowWidthScaled, windowHeightScaled)); @@ -210,7 +209,7 @@ namespace Ryujinx.Ava.UI.Views.Main public async void CheckForUpdates(object sender, RoutedEventArgs e) { if (Updater.CanUpdate(true)) - await Window.BeginUpdateAsync(true); + await Updater.BeginUpdateAsync(true); } public async void OpenXCITrimmerWindow(object sender, RoutedEventArgs e) => await XCITrimmerWindow.Show(ViewModel); diff --git a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml index 597cf10e1..6e72a8b4b 100644 --- a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml @@ -23,7 +23,7 @@ Background="{DynamicResource ThemeContentBackgroundColor}" DockPanel.Dock="Bottom" IsVisible="{Binding ShowMenuAndStatusBar}" - ColumnDefinitions="Auto,Auto,*,Auto"> + ColumnDefinitions="Auto,Auto,*,Auto,Auto"> + + + + IsVisible="{Binding ShowShaderCompilationHint}" /> + + - - - + IsVisible="{Binding IsGameRunning}" /> diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 059f99a60..09c8b9448 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -7,6 +7,7 @@ using Avalonia.Threading; using DynamicData; using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Windowing; +using Gommon; using LibHac.Tools.FsSystem; using Ryujinx.Ava.Common; using Ryujinx.Ava.Common.Locale; @@ -387,10 +388,8 @@ namespace Ryujinx.Ava.UI.Windows if (ConfigurationState.Instance.CheckUpdatesOnStart && !CommandLineState.HideAvailableUpdates && Updater.CanUpdate()) { - await this.BeginUpdateAsync() - .ContinueWith( - task => Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}"), - TaskContinuationOptions.OnlyOnFaulted); + await Updater.BeginUpdateAsync() + .Catch(task => Logger.Error?.Print(LogClass.Application, $"Updater Error: {task.Exception}")); } } diff --git a/src/Ryujinx/Updater.cs b/src/Ryujinx/Updater.cs index bdb44d668..6a1701208 100644 --- a/src/Ryujinx/Updater.cs +++ b/src/Ryujinx/Updater.cs @@ -1,4 +1,3 @@ -using Avalonia.Controls; using Avalonia.Threading; using FluentAvalonia.UI.Controls; using Gommon; @@ -51,7 +50,7 @@ namespace Ryujinx.Ava private static readonly string[] _windowsDependencyDirs = []; - public static async Task BeginUpdateAsync(this Window mainWindow, bool showVersionUpToDate = false) + public static async Task BeginUpdateAsync(bool showVersionUpToDate = false) { if (_running) { @@ -225,7 +224,7 @@ namespace Ryujinx.Ava ? $"Canary {currentVersion} -> Canary {newVersion}" : $"{currentVersion} -> {newVersion}"; - RequestUserToUpdate: + RequestUserToUpdate: // Show a message asking the user if they want to update UserResult shouldUpdate = await ContentDialogHelper.CreateUpdaterChoiceDialog( LocaleManager.Instance[LocaleKeys.RyujinxUpdater], @@ -235,7 +234,7 @@ namespace Ryujinx.Ava switch (shouldUpdate) { case UserResult.Yes: - await UpdateRyujinx(mainWindow, _buildUrl); + await UpdateRyujinx(_buildUrl); break; // Secondary button maps to no, which in this case is the show changelog button. case UserResult.No: @@ -258,7 +257,7 @@ namespace Ryujinx.Ava return result; } - private static async Task UpdateRyujinx(Window parent, string downloadUrl) + private static async Task UpdateRyujinx(string downloadUrl) { _updateSuccessful = false; @@ -278,7 +277,7 @@ namespace Ryujinx.Ava SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterDownloading], IconSource = new SymbolIconSource { Symbol = Symbol.Download }, ShowProgressBar = true, - XamlRoot = parent, + XamlRoot = App.MainWindow, }; taskDialog.Opened += (s, e) => From 000c1756de0851a2d4bd2f458e5e987c3cba66dd Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Fri, 6 Dec 2024 08:17:04 -0600 Subject: [PATCH 03/79] version 1.2 in Info.plist --- distribution/macos/Info.plist | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distribution/macos/Info.plist b/distribution/macos/Info.plist index 53929f95e..2602f9905 100644 --- a/distribution/macos/Info.plist +++ b/distribution/macos/Info.plist @@ -40,11 +40,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.1 + 1.2 CFBundleSignature ???? CFBundleVersion - 1.1.0 + 1.2.0 NSHighResolutionCapable CSResourcesFileMapped From 3d168a8bfa7bd5a418b50ce7a82a7780d4c3b5f5 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Fri, 6 Dec 2024 08:18:24 -0600 Subject: [PATCH 04/79] direct errored updates to ryujinx.app --- distribution/macos/updater.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/distribution/macos/updater.sh b/distribution/macos/updater.sh index 12e4c3aa1..0465d7c91 100755 --- a/distribution/macos/updater.sh +++ b/distribution/macos/updater.sh @@ -17,7 +17,7 @@ error_handler() { set the button_pressed to the button returned of the result if the button_pressed is \"Open Download Page\" then - open location \"https://ryujinx.org/download\" + open location \"https://ryujinx.app/download\" end if """ @@ -54,4 +54,4 @@ if [ "$#" -le 3 ]; then open -a "$INSTALL_DIRECTORY" else open -a "$INSTALL_DIRECTORY" --args "${APP_ARGUMENTS[@]}" -fi \ No newline at end of file +fi From a1e6d11dcb0b125b1a953b5ba81d3b39aeecbff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hack=E8=8C=B6=E3=82=93?= <120134269+Hackjjang@users.noreply.github.com> Date: Sat, 7 Dec 2024 00:18:09 +0900 Subject: [PATCH 05/79] Update Korean translation (#352) --- src/Ryujinx/Assets/Locales/ko_KR.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Ryujinx/Assets/Locales/ko_KR.json b/src/Ryujinx/Assets/Locales/ko_KR.json index 6b7140b3c..8731c8662 100644 --- a/src/Ryujinx/Assets/Locales/ko_KR.json +++ b/src/Ryujinx/Assets/Locales/ko_KR.json @@ -1,7 +1,7 @@ { "Language": "한국어", "MenuBarFileOpenApplet": "애플릿 열기", - "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", + "MenuBarFileOpenAppletOpenMiiApplet": "Mii 편집 애플릿", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "독립 실행형 모드로 Mii 편집기 애플릿 열기", "SettingsTabInputDirectMouseAccess": "마우스 직접 접근", "SettingsTabSystemMemoryManagerMode": "메모리 관리자 모드 :", @@ -484,7 +484,7 @@ "DialogControllerAppletTitle": "컨트롤러 애플릿", "DialogMessageDialogErrorExceptionMessage": "메시지 대화 상자 표시 오류 : {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "소프트웨어 키보드 표시 오류 : {0}", - "DialogErrorAppletErrorExceptionMessage": "ErrorApplet 대화 상자 표시 오류 : {0}", + "DialogErrorAppletErrorExceptionMessage": "애플릿 오류류 대화 상자 표시 오류 : {0}", "DialogUserErrorDialogMessage": "{0}: {1}", "DialogUserErrorDialogInfoMessage": "\n이 오류를 해결하는 방법에 대한 자세한 내용은 설정 가이드를 참조하세요.", "DialogUserErrorDialogTitle": "Ryujinx 오류 ({0})", @@ -702,9 +702,9 @@ "Never": "절대 안 함", "SwkbdMinCharacters": "{0}자 이상이어야 함", "SwkbdMinRangeCharacters": "{0}-{1}자 길이여야 함", - "CabinetTitle": "Cabinet Dialog", - "CabinetDialog": "Enter your Amiibo's new name", - "CabinetScanDialog": "Please scan your Amiibo now.", + "CabinetTitle": "캐비닛 대화 상자", + "CabinetDialog": "Amiibo의 새 이름 입력하기", + "CabinetScanDialog": "지금 Amiibo를 스캔하세요.", "SoftwareKeyboard": "소프트웨어 키보드", "SoftwareKeyboardModeNumeric": "0-9 또는 '.'만 가능", "SoftwareKeyboardModeAlphabet": "CJK 문자가 아닌 문자만 가능", @@ -781,8 +781,8 @@ "XCITrimmerDeselectDisplayed": "표시됨 선택 취소", "XCITrimmerSortName": "타이틀", "XCITrimmerSortSaved": "공간 절약s", - "XCITrimmerTrim": "Trim", - "XCITrimmerUntrim": "Untrim", + "XCITrimmerTrim": "트림", + "XCITrimmerUntrim": "언트림", "UpdateWindowUpdateAddedMessage": "{0}개의 새 업데이트가 추가됨", "UpdateWindowBundledContentNotice": "번들 업데이트는 제거할 수 없으며, 비활성화만 가능합니다.", "CheatWindowHeading": "{0} [{1}]에 사용 가능한 치트", From baad1e313f80bf53a20fc1ee04fdc716b1728bf1 Mon Sep 17 00:00:00 2001 From: Luke Warner <65521430+LukeWarnut@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:43:31 -0500 Subject: [PATCH 06/79] Stub Ldn.Lp2p.ISfService: 776 (DestroyGroup) (#353) This prevents a crash in Mario Kart Live: Home Circuit that would occur after exiting the kart pairing screen. --- src/Ryujinx.HLE/HOS/Services/Ldn/Lp2p/ISfService.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/Lp2p/ISfService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/Lp2p/ISfService.cs index d48a88978..8f9f0e3e4 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/Lp2p/ISfService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/Lp2p/ISfService.cs @@ -24,6 +24,15 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.Lp2p return ResultCode.Success; } + [CommandCmif(776)] + // DestroyGroup() + public ResultCode DestroyGroup(ServiceCtx context) + { + Logger.Stub?.PrintStub(LogClass.ServiceLdn); + + return ResultCode.Success; + } + [CommandCmif(1536)] // SendToOtherGroup(nn::lp2p::MacAddress, nn::lp2p::GroupId, s16, s16, u32, buffer) public ResultCode SendToOtherGroup(ServiceCtx context) From 0bc1eddaebc03d790fa0c16729382967f7f229dc Mon Sep 17 00:00:00 2001 From: maxdlpee <77379259+maxdlpee@users.noreply.github.com> Date: Sat, 7 Dec 2024 00:57:35 -0300 Subject: [PATCH 07/79] Update Spanish translation (#332) - Added translations for XCI trimmer - Added translations for Cabinet applet - Added translations for Keys installer - Other miscellaneous translations added --- src/Ryujinx/Assets/Locales/es_ES.json | 108 +++++++++++++------------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/src/Ryujinx/Assets/Locales/es_ES.json b/src/Ryujinx/Assets/Locales/es_ES.json index b473b1197..934031c72 100644 --- a/src/Ryujinx/Assets/Locales/es_ES.json +++ b/src/Ryujinx/Assets/Locales/es_ES.json @@ -1,7 +1,7 @@ { "Language": "Español (ES)", "MenuBarFileOpenApplet": "Abrir applet", - "MenuBarFileOpenAppletOpenMiiApplet": "Mii Edit Applet", + "MenuBarFileOpenAppletOpenMiiApplet": "Applet Editor Mii", "MenuBarFileOpenAppletOpenMiiAppletToolTip": "Abre el editor de Mii en modo autónomo", "SettingsTabInputDirectMouseAccess": "Acceso directo al ratón", "SettingsTabSystemMemoryManagerMode": "Modo del administrador de memoria:", @@ -32,12 +32,12 @@ "MenuBarFileToolsInstallFirmwareFromFile": "Instalar firmware desde un archivo XCI o ZIP", "MenuBarFileToolsInstallFirmwareFromDirectory": "Instalar firmware desde una carpeta", "MenuBarToolsInstallKeys": "Install Keys", - "MenuBarFileToolsInstallKeysFromFile": "Install keys from KEYS or ZIP", - "MenuBarFileToolsInstallKeysFromFolder": "Install keys from a directory", + "MenuBarFileToolsInstallKeysFromFile": "Instalar keys de KEYS o ZIP", + "MenuBarFileToolsInstallKeysFromFolder": "Instalar keys de un directorio", "MenuBarToolsManageFileTypes": "Administrar tipos de archivo", "MenuBarToolsInstallFileTypes": "Instalar tipos de archivo", "MenuBarToolsUninstallFileTypes": "Desinstalar tipos de archivo", - "MenuBarToolsXCITrimmer": "Trim XCI Files", + "MenuBarToolsXCITrimmer": "Recortar archivos XCI", "MenuBarView": "_View", "MenuBarViewWindow": "Tamaño Ventana", "MenuBarViewWindow720": "720p", @@ -89,11 +89,11 @@ "GameListContextMenuOpenModsDirectoryToolTip": "Abre el directorio que contiene los Mods de la Aplicación.", "GameListContextMenuOpenSdModsDirectory": "Abrir Directorio de Mods de Atmosphere\n\n\n\n\n\n", "GameListContextMenuOpenSdModsDirectoryToolTip": "Abre el directorio alternativo de la tarjeta SD de Atmosphere que contiene los Mods de la Aplicación. Útil para los mods que están empaquetados para el hardware real.", - "GameListContextMenuTrimXCI": "Check and Trim XCI File", - "GameListContextMenuTrimXCIToolTip": "Check and Trim XCI File to Save Disk Space", + "GameListContextMenuTrimXCI": "Verificar y recortar archivo XCI", + "GameListContextMenuTrimXCIToolTip": "Verificar y recortar archivo XCI para ahorrar espacio en disco", "StatusBarGamesLoaded": "{0}/{1} juegos cargados", "StatusBarSystemVersion": "Versión del sistema: {0}", - "StatusBarXCIFileTrimming": "Trimming XCI File '{0}'", + "StatusBarXCIFileTrimming": "Recortando el siguiente archivo XCI: '{0}'", "LinuxVmMaxMapCountDialogTitle": "Límite inferior para mapeos de memoria detectado", "LinuxVmMaxMapCountDialogTextPrimary": "¿Quieres aumentar el valor de vm.max_map_count a {0}?", "LinuxVmMaxMapCountDialogTextSecondary": "Algunos juegos podrían intentar crear más mapeos de memoria de los permitidos. Ryujinx se bloqueará tan pronto como se supere este límite.", @@ -480,7 +480,7 @@ "DialogUninstallFileTypesSuccessMessage": "¡Tipos de archivos desinstalados con éxito!", "DialogUninstallFileTypesErrorMessage": "No se pudo desinstalar los tipos de archivo.", "DialogOpenSettingsWindowLabel": "Abrir ventana de opciones", - "DialogOpenXCITrimmerWindowLabel": "XCI Trimmer Window", + "DialogOpenXCITrimmerWindowLabel": "Ventana recortador XCI", "DialogControllerAppletTitle": "Applet de mandos", "DialogMessageDialogErrorExceptionMessage": "Error al mostrar cuadro de diálogo: {0}", "DialogSoftwareKeyboardErrorExceptionMessage": "Error al mostrar teclado de software: {0}", @@ -509,13 +509,13 @@ "DialogFirmwareInstallerFirmwareInstallConfirmMessage": "\n\n¿Continuar?", "DialogFirmwareInstallerFirmwareInstallWaitMessage": "Instalando firmware...", "DialogFirmwareInstallerFirmwareInstallSuccessMessage": "Versión de sistema {0} instalada con éxito.", - "DialogKeysInstallerKeysNotFoundErrorMessage": "An invalid Keys file was found in {0}", - "DialogKeysInstallerKeysInstallTitle": "Install Keys", - "DialogKeysInstallerKeysInstallMessage": "New Keys file will be installed.", - "DialogKeysInstallerKeysInstallSubMessage": "\n\nThis may replace some of the current installed Keys.", - "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDo you want to continue?", - "DialogKeysInstallerKeysInstallWaitMessage": "Installing Keys...", - "DialogKeysInstallerKeysInstallSuccessMessage": "New Keys file successfully installed.", + "DialogKeysInstallerKeysNotFoundErrorMessage": "Se halló un archivo Keys inválido en {0}", + "DialogKeysInstallerKeysInstallTitle": "Instalar Keys", + "DialogKeysInstallerKeysInstallMessage": "Un nuevo archivo Keys será instalado.", + "DialogKeysInstallerKeysInstallSubMessage": "\n\nEsto puede reemplazar algunas de las Keys actualmente instaladas.", + "DialogKeysInstallerKeysInstallConfirmMessage": "\n\nDeseas continuar?", + "DialogKeysInstallerKeysInstallWaitMessage": "Instalando Keys...", + "DialogKeysInstallerKeysInstallSuccessMessage": "Nuevo archivo Keys instalado con éxito.", "DialogUserProfileDeletionWarningMessage": "Si eliminas el perfil seleccionado no quedará ningún otro perfil", "DialogUserProfileDeletionConfirmMessage": "¿Quieres eliminar el perfil seleccionado?", "DialogUserProfileUnsavedChangesTitle": "Advertencia - Cambios sin guardar", @@ -688,23 +688,23 @@ "OpenSetupGuideMessage": "Abrir la guía de instalación", "NoUpdate": "No actualizado", "TitleUpdateVersionLabel": "Versión {0} - {1}", - "TitleBundledUpdateVersionLabel": "Bundled: Version {0}", - "TitleBundledDlcLabel": "Bundled:", - "TitleXCIStatusPartialLabel": "Partial", - "TitleXCIStatusTrimmableLabel": "Untrimmed", - "TitleXCIStatusUntrimmableLabel": "Trimmed", - "TitleXCIStatusFailedLabel": "(Failed)", - "TitleXCICanSaveLabel": "Save {0:n0} Mb", - "TitleXCISavingLabel": "Saved {0:n0} Mb", + "TitleBundledUpdateVersionLabel": "Incorporado: Versión {0}", + "TitleBundledDlcLabel": "Incorporado:", + "TitleXCIStatusPartialLabel": "Parcial", + "TitleXCIStatusTrimmableLabel": "Sin recortar", + "TitleXCIStatusUntrimmableLabel": "Recortado", + "TitleXCIStatusFailedLabel": "(Fallido)", + "TitleXCICanSaveLabel": "Ahorra {0:n0} Mb", + "TitleXCISavingLabel": "{0:n0} Mb ahorrado(s)", "RyujinxInfo": "Ryujinx - Info", "RyujinxConfirm": "Ryujinx - Confirmación", "FileDialogAllTypes": "Todos los tipos", "Never": "Nunca", "SwkbdMinCharacters": "Debe tener al menos {0} caracteres", "SwkbdMinRangeCharacters": "Debe tener {0}-{1} caracteres", - "CabinetTitle": "Cabinet Dialog", - "CabinetDialog": "Enter your Amiibo's new name", - "CabinetScanDialog": "Please scan your Amiibo now.", + "CabinetTitle": "Diálogo Gabinete", + "CabinetDialog": "Ingresa el nuevo nombre de tu Amiibo", + "CabinetScanDialog": "Escanea tu Amiibo ahora.", "SoftwareKeyboard": "Teclado de software", "SoftwareKeyboardModeNumeric": "Debe ser sólo 0-9 o '.'", "SoftwareKeyboardModeAlphabet": "Solo deben ser caracteres no CJK", @@ -750,39 +750,39 @@ "SelectDlcDialogTitle": "Selecciona archivo(s) de DLC", "SelectUpdateDialogTitle": "Selecciona archivo(s) de actualización", "SelectModDialogTitle": "Seleccionar un directorio de Mods", - "TrimXCIFileDialogTitle": "Check and Trim XCI File", - "TrimXCIFileDialogPrimaryText": "This function will first check the empty space and then trim the XCI File to save disk space.", - "TrimXCIFileDialogSecondaryText": "Current File Size: {0:n} MB\nGame Data Size: {1:n} MB\nDisk Space Savings: {2:n} MB", - "TrimXCIFileNoTrimNecessary": "XCI File does not need to be trimmed. Check logs for further details", - "TrimXCIFileNoUntrimPossible": "XCI File cannot be untrimmed. Check logs for further details", - "TrimXCIFileReadOnlyFileCannotFix": "XCI File is Read Only and could not be made writable. Check logs for further details", - "TrimXCIFileFileSizeChanged": "XCI File has changed in size since it was scanned. Please check the file is not being written to and try again.", - "TrimXCIFileFreeSpaceCheckFailed": "XCI File has data in the free space area, it is not safe to trim", - "TrimXCIFileInvalidXCIFile": "XCI File contains invalid data. Check logs for further details", - "TrimXCIFileFileIOWriteError": "XCI File could not be opened for writing. Check logs for further details", - "TrimXCIFileFailedPrimaryText": "Trimming of the XCI file failed", - "TrimXCIFileCancelled": "The operation was cancelled", - "TrimXCIFileFileUndertermined": "No operation was performed", + "TrimXCIFileDialogTitle": "Verificar y recortar archivo XCI", + "TrimXCIFileDialogPrimaryText": "Esta función verificará el espacio vacío y después recortará el archivo XCI para ahorrar espacio en disco", + "TrimXCIFileDialogSecondaryText": "Tamaño de archivo actual: {0:n} MB\nTamaño de datos de juego: {1:n} MB\nAhorro de espacio en disco: {2:n} MB", + "TrimXCIFileNoTrimNecessary": "El archivo XCI no necesita ser recortado. Verifica los logs para más detalles.", + "TrimXCIFileNoUntrimPossible": "El recorte del archivo XCI no puede ser deshecho. Verifica los registros para más detalles.", + "TrimXCIFileReadOnlyFileCannotFix": "El archivo XCI es de solo Lectura y no se le puede escribir. Lee el registro para más información.", + "TrimXCIFileFileSizeChanged": "El archivo XCI ha cambiado de tamaño desde que fue escaneado. Verifica que no se esté escribiendo al archivo y vuelve a intentarlo.", + "TrimXCIFileFreeSpaceCheckFailed": "El archivo XCI tiene datos en el área de espacio libre, no es seguro recortar.", + "TrimXCIFileInvalidXCIFile": "El archivo XCI contiene datos inválidos. Lee el registro para más información.", + "TrimXCIFileFileIOWriteError": "El archivo XCI no se puede abrir para escribirlo. Lee el registro para más información.", + "TrimXCIFileFailedPrimaryText": "El recorte del archivo XCI falló", + "TrimXCIFileCancelled": "La operación fue cancelada", + "TrimXCIFileFileUndertermined": "No se realizó ninguna operación", "UserProfileWindowTitle": "Administrar perfiles de usuario", "CheatWindowTitle": "Administrar cheats", "DlcWindowTitle": "Administrar contenido descargable", "ModWindowTitle": "Administrar Mods para {0} ({1})", "UpdateWindowTitle": "Administrar actualizaciones", - "XCITrimmerWindowTitle": "XCI File Trimmer", - "XCITrimmerTitleStatusCount": "{0} of {1} Title(s) Selected", - "XCITrimmerTitleStatusCountWithFilter": "{0} of {1} Title(s) Selected ({2} displayed)", - "XCITrimmerTitleStatusTrimming": "Trimming {0} Title(s)...", - "XCITrimmerTitleStatusUntrimming": "Untrimming {0} Title(s)...", - "XCITrimmerTitleStatusFailed": "Failed", - "XCITrimmerPotentialSavings": "Potential Savings", - "XCITrimmerActualSavings": "Actual Savings", + "XCITrimmerWindowTitle": "Recortador de archivos XCI", + "XCITrimmerTitleStatusCount": "{0} de {1} Título(s) seleccionado(s)", + "XCITrimmerTitleStatusCountWithFilter": "{0} de {1} Título(s) seleccionado(s) ({2} mostrado(s))", + "XCITrimmerTitleStatusTrimming": "Recortando {0} Título(s)...", + "XCITrimmerTitleStatusUntrimming": "Deshaciendo recorte de {0} Título(s)...", + "XCITrimmerTitleStatusFailed": "Fallido", + "XCITrimmerPotentialSavings": "Ahorro potencial", + "XCITrimmerActualSavings": "Ahorro real", "XCITrimmerSavingsMb": "{0:n0} Mb", - "XCITrimmerSelectDisplayed": "Select Shown", - "XCITrimmerDeselectDisplayed": "Deselect Shown", - "XCITrimmerSortName": "Title", - "XCITrimmerSortSaved": "Space Savings", - "XCITrimmerTrim": "Trim", - "XCITrimmerUntrim": "Untrim", + "XCITrimmerSelectDisplayed": "Seleccionar mostrado(s)", + "XCITrimmerDeselectDisplayed": "Deseleccionar mostrado(s)", + "XCITrimmerSortName": "Título", + "XCITrimmerSortSaved": "Ahorro de espacio", + "XCITrimmerTrim": "Recortar", + "XCITrimmerUntrim": "Deshacer recorte", "UpdateWindowUpdateAddedMessage": "{0} nueva(s) actualización(es) agregada(s)", "UpdateWindowBundledContentNotice": "Las actualizaciones agrupadas no pueden ser eliminadas, solamente deshabilitadas.", "CheatWindowHeading": "Cheats disponibles para {0} [{1}]", @@ -795,7 +795,7 @@ "AutoloadUpdateRemovedMessage": "Se eliminaron {0} actualización(es) faltantes", "ModWindowHeading": "{0} Mod(s)", "UserProfilesEditProfile": "Editar selección", - "Continue": "Continue", + "Continue": "Continuar", "Cancel": "Cancelar", "Save": "Guardar", "Discard": "Descartar", From d00754477eab8ec47ed3824d96b3a766dfe93bc2 Mon Sep 17 00:00:00 2001 From: WilliamWsyHK Date: Sat, 7 Dec 2024 18:03:01 +0800 Subject: [PATCH 08/79] Add Firmware keyword in log if it is indeed firmware (#343) Co-authored-by: LotP1 --- .../Processes/Extensions/NcaExtensions.cs | 6 ++++- .../Loaders/Processes/ProcessLoader.cs | 4 +-- .../Loaders/Processes/ProcessResult.cs | 13 ++++++--- src/Ryujinx.HLE/Switch.cs | 4 ++- src/Ryujinx/AppHost.cs | 6 +++-- .../UI/ViewModels/MainWindowViewModel.cs | 5 ++-- .../UI/Views/Main/MainMenuBarView.axaml.cs | 27 ++++++++++++++++--- 7 files changed, 50 insertions(+), 15 deletions(-) diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs index 2928ac7fe..361a9159e 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs @@ -26,7 +26,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions { private static readonly TitleUpdateMetadataJsonSerializerContext _applicationSerializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca) + public static ProcessResult Load(this Nca nca, Switch device, Nca patchNca, Nca controlNca, BlitStruct? customNacpData = null) { // Extract RomFs and ExeFs from NCA. IStorage romFs = nca.GetRomFs(device, patchNca); @@ -55,6 +55,10 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions { nacpData = controlNca.GetNacp(device); } + else if (customNacpData != null) // if the Application doesn't provide a nacp file but the Application provides an override, use the provided nacp override + { + nacpData = (BlitStruct)customNacpData; + } /* TODO: Rework this since it's wrong and doesn't work as it takes the DisplayVersion from a "potential" non-existent update. diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs index a0e7e0fa1..fe8360f04 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs @@ -98,12 +98,12 @@ namespace Ryujinx.HLE.Loaders.Processes return false; } - public bool LoadNca(string path) + public bool LoadNca(string path, BlitStruct? customNacpData = null) { FileStream file = new(path, FileMode.Open, FileAccess.Read); Nca nca = new(_device.Configuration.VirtualFileSystem.KeySet, file.AsStorage(false)); - ProcessResult processResult = nca.Load(_device, null, null); + ProcessResult processResult = nca.Load(_device, null, null, customNacpData); if (processResult.ProcessId != 0 && _processesByPid.TryAdd(processResult.ProcessId, processResult)) { diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs index e187b2360..3a7042670 100644 --- a/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs +++ b/src/Ryujinx.HLE/Loaders/Processes/ProcessResult.cs @@ -84,12 +84,19 @@ namespace Ryujinx.HLE.Loaders.Processes return false; } + bool isFirmware = ProgramId is >= 0x0100000000000819 and <= 0x010000000000081C; + bool isFirmwareApplication = ProgramId <= 0x0100000000007FFF; + + string name = !isFirmware + ? (isFirmwareApplication ? "Firmware Application " : "") + (!string.IsNullOrWhiteSpace(Name) ? Name : "") + : "Firmware"; + // TODO: LibHac npdm currently doesn't support version field. - string version = ProgramId > 0x0100000000007FFF - ? DisplayVersion + string version = !isFirmware + ? (!string.IsNullOrWhiteSpace(DisplayVersion) ? DisplayVersion : "") : device.System.ContentManager.GetCurrentFirmwareVersion()?.VersionString ?? "?"; - Logger.Info?.Print(LogClass.Loader, $"Application Loaded: {Name} v{version} [{ProgramIdText}] [{(Is64Bit ? "64-bit" : "32-bit")}]"); + Logger.Info?.Print(LogClass.Loader, $"Application Loaded: {name} v{version} [{ProgramIdText}] [{(Is64Bit ? "64-bit" : "32-bit")}]"); return true; } diff --git a/src/Ryujinx.HLE/Switch.cs b/src/Ryujinx.HLE/Switch.cs index 466352152..d0afdf173 100644 --- a/src/Ryujinx.HLE/Switch.cs +++ b/src/Ryujinx.HLE/Switch.cs @@ -1,3 +1,5 @@ +using LibHac.Common; +using LibHac.Ns; using Ryujinx.Audio.Backends.CompatLayer; using Ryujinx.Audio.Integration; using Ryujinx.Common.Configuration; @@ -111,7 +113,7 @@ namespace Ryujinx.HLE public bool LoadCart(string exeFsDir, string romFsFile = null) => Processes.LoadUnpackedNca(exeFsDir, romFsFile); public bool LoadXci(string xciFile, ulong applicationId = 0) => Processes.LoadXci(xciFile, applicationId); - public bool LoadNca(string ncaFile) => Processes.LoadNca(ncaFile); + public bool LoadNca(string ncaFile, BlitStruct? customNacpData = null) => Processes.LoadNca(ncaFile, customNacpData); public bool LoadNsp(string nspFile, ulong applicationId = 0) => Processes.LoadNsp(nspFile, applicationId); public bool LoadProgram(string fileName) => Processes.LoadNxo(fileName); diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index 9a7f82661..65c798ac2 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -3,6 +3,8 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input; using Avalonia.Threading; +using LibHac.Common; +using LibHac.Ns; using LibHac.Tools.FsSystem; using Ryujinx.Audio.Backends.Dummy; using Ryujinx.Audio.Backends.OpenAL; @@ -670,7 +672,7 @@ namespace Ryujinx.Ava _cursorState = CursorStates.ForceChangeCursor; } - public async Task LoadGuestApplication() + public async Task LoadGuestApplication(BlitStruct? customNacpData = null) { InitializeSwitchInstance(); MainWindow.UpdateGraphicsConfig(); @@ -740,7 +742,7 @@ namespace Ryujinx.Ava { Logger.Info?.Print(LogClass.Application, "Loading as Firmware Title (NCA)."); - if (!Device.LoadNca(ApplicationPath)) + if (!Device.LoadNca(ApplicationPath, customNacpData)) { Device.Dispose(); diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 1bfcd439b..04db947b9 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -10,6 +10,7 @@ using DynamicData; using DynamicData.Binding; using FluentAvalonia.UI.Controls; using LibHac.Common; +using LibHac.Ns; using Ryujinx.Ava.Common; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Input; @@ -1897,7 +1898,7 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public async Task LoadApplication(ApplicationData application, bool startFullscreen = false) + public async Task LoadApplication(ApplicationData application, bool startFullscreen = false, BlitStruct? customNacpData = null) { if (AppHost != null) { @@ -1935,7 +1936,7 @@ namespace Ryujinx.Ava.UI.ViewModels this, TopLevel); - if (!await AppHost.LoadGuestApplication()) + if (!await AppHost.LoadGuestApplication(customNacpData)) { AppHost.DisposeContext(); AppHost = null; diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs index a3aa58f2c..94f5cf9d3 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -3,7 +3,9 @@ using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Threading; using Gommon; +using LibHac.Common; using LibHac.Ncm; +using LibHac.Ns; using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Helpers; @@ -19,6 +21,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text; namespace Ryujinx.Ava.UI.Views.Main { @@ -123,18 +126,34 @@ namespace Ryujinx.Ava.UI.Views.Main public async void OpenMiiApplet(object sender, RoutedEventArgs e) { - string contentPath = ViewModel.ContentManager.GetInstalledContentPath(0x0100000000001009, StorageId.BuiltInSystem, NcaContentType.Program); + const string name = "miiEdit"; + const ulong programId = 0x0100000000001009; + string contentPath = ViewModel.ContentManager.GetInstalledContentPath(programId, StorageId.BuiltInSystem, NcaContentType.Program); if (!string.IsNullOrEmpty(contentPath)) { ApplicationData applicationData = new() { - Name = "miiEdit", - Id = 0x0100000000001009, + Name = name, + Id = programId, Path = contentPath, }; - await ViewModel.LoadApplication(applicationData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen); + string version = "1.0.0"; + var nacpData = new BlitStruct(1); + + //version buffer + Encoding.ASCII.GetBytes(version).AsSpan().CopyTo(nacpData.ByteSpan.Slice(0x3060)); + + //name and distributor buffer + //repeat once for each locale (the ApplicationControlProperty has 16 locales) + for (int i = 0; i < 0x10; i++) + { + Encoding.ASCII.GetBytes(name).AsSpan().CopyTo(nacpData.ByteSpan.Slice(i * 0x300)); + "Ryujinx"u8.ToArray().AsSpan().CopyTo(nacpData.ByteSpan.Slice(i * 0x300 + 0x200)); + } + + await ViewModel.LoadApplication(applicationData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen, nacpData); } } From 5fbcb1f3a7b7f18a120db350f6b1fb3bd6cb305d Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 7 Dec 2024 04:05:39 -0600 Subject: [PATCH 09/79] misc: chore: Cleanups & unused parameter removal --- .../Controller/JoyconConfigControllerStick.cs | 4 +- .../App/ApplicationLibrary.cs | 39 +++++++------------ .../Configuration/ConfigurationState.cs | 8 +--- .../Helper/DownloadableContentsHelper.cs | 2 +- .../Helper/TitleUpdatesHelper.cs | 2 +- 5 files changed, 22 insertions(+), 33 deletions(-) diff --git a/src/Ryujinx.Common/Configuration/Hid/Controller/JoyconConfigControllerStick.cs b/src/Ryujinx.Common/Configuration/Hid/Controller/JoyconConfigControllerStick.cs index 608681551..076530744 100644 --- a/src/Ryujinx.Common/Configuration/Hid/Controller/JoyconConfigControllerStick.cs +++ b/src/Ryujinx.Common/Configuration/Hid/Controller/JoyconConfigControllerStick.cs @@ -1,6 +1,8 @@ namespace Ryujinx.Common.Configuration.Hid.Controller { - public class JoyconConfigControllerStick where TButton : unmanaged where TStick : unmanaged + public class JoyconConfigControllerStick + where TButton : unmanaged + where TStick : unmanaged { public TStick Joystick { get; set; } public bool InvertStickX { get; set; } diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index 174db51ad..cc5a63ab8 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -1,5 +1,4 @@ using DynamicData; -using DynamicData.Kernel; using Gommon; using LibHac; using LibHac.Common; @@ -37,14 +36,13 @@ using System.Threading.Tasks; using ContentType = LibHac.Ncm.ContentType; using MissingKeyException = LibHac.Common.Keys.MissingKeyException; using Path = System.IO.Path; -using SpanHelpers = LibHac.Common.SpanHelpers; using TimeSpan = System.TimeSpan; namespace Ryujinx.UI.App.Common { public class ApplicationLibrary { - public static string DefaultLanPlayWebHost = "ryuldnweb.vudjun.com"; + public const string DefaultLanPlayWebHost = "ryuldnweb.vudjun.com"; public Language DesiredLanguage { get; set; } public event EventHandler ApplicationCountUpdated; public event EventHandler LdnGameDataReceived; @@ -191,12 +189,9 @@ namespace Ryujinx.UI.App.Common } } - if (isExeFs) - { - return GetApplicationFromExeFs(pfs, filePath); - } - - return null; + return isExeFs + ? GetApplicationFromExeFs(pfs, filePath) + : null; } /// The configured key set is missing a key. @@ -512,10 +507,6 @@ namespace Ryujinx.UI.App.Common case ".xci": case ".nsp": { - IntegrityCheckLevel checkLevel = ConfigurationState.Instance.System.EnableFsIntegrityChecks - ? IntegrityCheckLevel.ErrorOnInvalid - : IntegrityCheckLevel.None; - using IFileSystem pfs = PartitionFileSystemUtils.OpenApplicationFileSystem(filePath, _virtualFileSystem); foreach (DirectoryEntryEx fileEntry in pfs.EnumerateEntries("/", "*.nca")) @@ -604,7 +595,7 @@ namespace Ryujinx.UI.App.Common controlNca.OpenFileSystem(NcaSectionType.Data, IntegrityCheckLevel.None) .OpenFile(ref nacpFile.Ref, "/control.nacp".ToU8Span(), OpenMode.Read) .ThrowIfFailure(); - nacpFile.Get.Read(out _, 0, SpanHelpers.AsByteSpan(ref controlData), + nacpFile.Get.Read(out _, 0, LibHac.Common.SpanHelpers.AsByteSpan(ref controlData), ReadOption.None).ThrowIfFailure(); var displayVersion = controlData.DisplayVersionString.ToString(); @@ -827,7 +818,7 @@ namespace Ryujinx.UI.App.Common { _downloadableContents.Edit(it => { - DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, application.IdBase, dlcs); + DownloadableContentsHelper.SaveDownloadableContentsJson(application.IdBase, dlcs); it.Remove(it.Items.Where(item => item.Dlc.TitleIdBase == application.IdBase)); it.AddOrUpdate(dlcs); @@ -839,7 +830,7 @@ namespace Ryujinx.UI.App.Common { _titleUpdates.Edit(it => { - TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, application.IdBase, updates); + TitleUpdatesHelper.SaveTitleUpdatesJson(application.IdBase, updates); it.Remove(it.Items.Where(item => item.TitleUpdate.TitleIdBase == application.IdBase)); it.AddOrUpdate(updates); @@ -1088,7 +1079,7 @@ namespace Ryujinx.UI.App.Common private bool AddAndAutoSelectUpdate(TitleUpdateModel update) { - var currentlySelected = TitleUpdates.Items.FirstOrOptional(it => + var currentlySelected = TitleUpdates.Items.FindFirst(it => it.TitleUpdate.TitleIdBase == update.TitleIdBase && it.IsSelected); var shouldSelect = !currentlySelected.HasValue || @@ -1464,7 +1455,7 @@ namespace Ryujinx.UI.App.Common if (addedNewDlc) { var gameDlcs = it.Items.Where(dlc => dlc.Dlc.TitleIdBase == application.IdBase).ToList(); - DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, application.IdBase, + DownloadableContentsHelper.SaveDownloadableContentsJson(application.IdBase, gameDlcs); } } @@ -1483,7 +1474,7 @@ namespace Ryujinx.UI.App.Common TitleUpdatesHelper.LoadTitleUpdatesJson(_virtualFileSystem, application.IdBase); it.AddOrUpdate(savedUpdates); - var selectedUpdate = savedUpdates.FirstOrOptional(update => update.IsSelected); + var selectedUpdate = savedUpdates.FindFirst(update => update.IsSelected); if (TryGetTitleUpdatesFromFile(application.Path, out var bundledUpdates)) { @@ -1498,9 +1489,9 @@ namespace Ryujinx.UI.App.Common if (!selectedUpdate.HasValue || selectedUpdate.Value.Item1.Version < update.Version) { shouldSelect = true; - if (selectedUpdate.HasValue) + if (selectedUpdate) _titleUpdates.AddOrUpdate((selectedUpdate.Value.Item1, false)); - selectedUpdate = DynamicData.Kernel.Optional<(TitleUpdateModel, bool IsSelected)>.Create((update, true)); + selectedUpdate = (update, true); } modifiedVersion = modifiedVersion || shouldSelect; @@ -1513,7 +1504,7 @@ namespace Ryujinx.UI.App.Common if (updatesChanged) { var gameUpdates = it.Items.Where(update => update.TitleUpdate.TitleIdBase == application.IdBase).ToList(); - TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, application.IdBase, gameUpdates); + TitleUpdatesHelper.SaveTitleUpdatesJson(application.IdBase, gameUpdates); } } }); @@ -1525,14 +1516,14 @@ namespace Ryujinx.UI.App.Common private void SaveDownloadableContentsForGame(ulong titleIdBase) { var dlcs = DownloadableContents.Items.Where(dlc => dlc.Dlc.TitleIdBase == titleIdBase).ToList(); - DownloadableContentsHelper.SaveDownloadableContentsJson(_virtualFileSystem, titleIdBase, dlcs); + DownloadableContentsHelper.SaveDownloadableContentsJson(titleIdBase, dlcs); } // Save the _currently tracked_ update state for the game private void SaveTitleUpdatesForGame(ulong titleIdBase) { var updates = TitleUpdates.Items.Where(update => update.TitleUpdate.TitleIdBase == titleIdBase).ToList(); - TitleUpdatesHelper.SaveTitleUpdatesJson(_virtualFileSystem, titleIdBase, updates); + TitleUpdatesHelper.SaveTitleUpdatesJson(titleIdBase, updates); } // ApplicationData isnt live-updating (e.g. when an update gets applied) and so this is meant to trigger a refresh diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs index badb047df..04ddd442f 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -1,16 +1,12 @@ -using ARMeilleure; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; -using Ryujinx.Common.Configuration.Hid.Controller; using Ryujinx.Common.Configuration.Hid.Keyboard; using Ryujinx.Common.Configuration.Multiplayer; -using Ryujinx.Common.Logging; using Ryujinx.Graphics.Vulkan; using Ryujinx.HLE; using Ryujinx.UI.Common.Configuration.System; using Ryujinx.UI.Common.Configuration.UI; using System; -using System.Collections.Generic; namespace Ryujinx.UI.Common.Configuration { @@ -21,10 +17,10 @@ namespace Ryujinx.UI.Common.Configuration if (Instance != null) { throw new InvalidOperationException("Configuration is already initialized"); - } + } Instance = new ConfigurationState(); - } + } public ConfigurationFileFormat ToFileFormat() { diff --git a/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs b/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs index 3695c5c5c..020529b55 100644 --- a/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs +++ b/src/Ryujinx.UI.Common/Helper/DownloadableContentsHelper.cs @@ -42,7 +42,7 @@ namespace Ryujinx.UI.Common.Helper } } - public static void SaveDownloadableContentsJson(VirtualFileSystem vfs, ulong applicationIdBase, List<(DownloadableContentModel, bool IsEnabled)> dlcs) + public static void SaveDownloadableContentsJson(ulong applicationIdBase, List<(DownloadableContentModel, bool IsEnabled)> dlcs) { DownloadableContentContainer container = default; List downloadableContentContainerList = new(); diff --git a/src/Ryujinx.UI.Common/Helper/TitleUpdatesHelper.cs b/src/Ryujinx.UI.Common/Helper/TitleUpdatesHelper.cs index 18fbabd6d..c6bacfd91 100644 --- a/src/Ryujinx.UI.Common/Helper/TitleUpdatesHelper.cs +++ b/src/Ryujinx.UI.Common/Helper/TitleUpdatesHelper.cs @@ -49,7 +49,7 @@ namespace Ryujinx.UI.Common.Helper } } - public static void SaveTitleUpdatesJson(VirtualFileSystem vfs, ulong applicationIdBase, List<(TitleUpdateModel, bool IsSelected)> updates) + public static void SaveTitleUpdatesJson(ulong applicationIdBase, List<(TitleUpdateModel, bool IsSelected)> updates) { var titleUpdateWindowData = new TitleUpdateMetadata { From eda4f4349bdde7809ccbea44364634901c3c8c7b Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 7 Dec 2024 04:06:07 -0600 Subject: [PATCH 10/79] headless: Actually log the command line errors --- src/Ryujinx.Headless.SDL2/Program.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs index ff87a3845..12158176a 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -1,4 +1,5 @@ using CommandLine; +using Gommon; using LibHac.Tools.FsSystem; using Ryujinx.Audio.Backends.SDL2; using Ryujinx.Common; @@ -96,8 +97,13 @@ namespace Ryujinx.Headless.SDL2 } Parser.Default.ParseArguments(args) - .WithParsed(Load) - .WithNotParsed(errors => errors.Output()); + .WithParsed(Load) + .WithNotParsed(errors => + { + Logger.Error?.PrintMsg(LogClass.Application, "Error parsing command-line arguments:"); + + errors.ForEach(err => Logger.Error?.PrintMsg(LogClass.Application, $" - {err.Tag}")); + }); } private static InputConfig HandlePlayerConfiguration(string inputProfileName, string inputId, PlayerIndex index) @@ -579,8 +585,8 @@ namespace Ryujinx.Headless.SDL2 options.MultiplayerLanInterfaceId, Common.Configuration.Multiplayer.MultiplayerMode.Disabled, false, - "", - "", + string.Empty, + string.Empty, options.CustomVSyncInterval); return new Switch(configuration); From 290a6ad5de02251604c9a85bb8a5b162fa01e0b7 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 7 Dec 2024 04:30:04 -0600 Subject: [PATCH 11/79] HLE: extract custom NACP data functionality into a static helper for generic reuse elsewhere, and clarify magic numbers. --- src/Ryujinx.HLE/StructHelpers.cs | 37 +++++++++++++++++++ .../UI/Views/Main/MainMenuBarView.axaml.cs | 31 ++++++---------- 2 files changed, 48 insertions(+), 20 deletions(-) create mode 100644 src/Ryujinx.HLE/StructHelpers.cs diff --git a/src/Ryujinx.HLE/StructHelpers.cs b/src/Ryujinx.HLE/StructHelpers.cs new file mode 100644 index 000000000..6e6af8cea --- /dev/null +++ b/src/Ryujinx.HLE/StructHelpers.cs @@ -0,0 +1,37 @@ +using LibHac.Common; +using LibHac.Ns; +using System; +using System.Text; + +namespace Ryujinx.HLE +{ + public static class StructHelpers + { + public static BlitStruct CreateCustomNacpData(string name, string version) + { + // https://switchbrew.org/wiki/NACP + const int OffsetOfDisplayVersion = 0x3060; + + // https://switchbrew.org/wiki/NACP#ApplicationTitle + const int TotalApplicationTitles = 0x10; + const int SizeOfApplicationTitle = 0x300; + const int OffsetOfApplicationPublisherStrings = 0x200; + + + var nacpData = new BlitStruct(1); + + // name and publisher buffer + // repeat once for each locale (the ApplicationControlProperty has 16 locales) + for (int i = 0; i < TotalApplicationTitles; i++) + { + Encoding.ASCII.GetBytes(name).AsSpan().CopyTo(nacpData.ByteSpan[(i * SizeOfApplicationTitle)..]); + "Ryujinx"u8.CopyTo(nacpData.ByteSpan[(i * SizeOfApplicationTitle + OffsetOfApplicationPublisherStrings)..]); + } + + // version buffer + Encoding.ASCII.GetBytes(version).AsSpan().CopyTo(nacpData.ByteSpan[OffsetOfDisplayVersion..]); + + return nacpData; + } + } +} diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs index 94f5cf9d3..ffe9b066c 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -13,6 +13,7 @@ using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Ava.UI.Windows; using Ryujinx.Common; using Ryujinx.Common.Utilities; +using Ryujinx.HLE; using Ryujinx.UI.App.Common; using Ryujinx.UI.Common; using Ryujinx.UI.Common.Configuration; @@ -126,32 +127,22 @@ namespace Ryujinx.Ava.UI.Views.Main public async void OpenMiiApplet(object sender, RoutedEventArgs e) { - const string name = "miiEdit"; - const ulong programId = 0x0100000000001009; - string contentPath = ViewModel.ContentManager.GetInstalledContentPath(programId, StorageId.BuiltInSystem, NcaContentType.Program); + const string AppletName = "miiEdit"; + const ulong AppletProgramId = 0x0100000000001009; + const string AppletVersion = "1.0.0"; + + string contentPath = ViewModel.ContentManager.GetInstalledContentPath(AppletProgramId, StorageId.BuiltInSystem, NcaContentType.Program); if (!string.IsNullOrEmpty(contentPath)) { ApplicationData applicationData = new() { - Name = name, - Id = programId, - Path = contentPath, + Name = AppletName, + Id = AppletProgramId, + Path = contentPath }; - - string version = "1.0.0"; - var nacpData = new BlitStruct(1); - - //version buffer - Encoding.ASCII.GetBytes(version).AsSpan().CopyTo(nacpData.ByteSpan.Slice(0x3060)); - - //name and distributor buffer - //repeat once for each locale (the ApplicationControlProperty has 16 locales) - for (int i = 0; i < 0x10; i++) - { - Encoding.ASCII.GetBytes(name).AsSpan().CopyTo(nacpData.ByteSpan.Slice(i * 0x300)); - "Ryujinx"u8.ToArray().AsSpan().CopyTo(nacpData.ByteSpan.Slice(i * 0x300 + 0x200)); - } + + var nacpData = StructHelpers.CreateCustomNacpData(AppletName, AppletVersion); await ViewModel.LoadApplication(applicationData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen, nacpData); } From 4ffb8aef129ac4b0469c862cc595aa97816d4808 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 7 Dec 2024 05:21:16 -0600 Subject: [PATCH 12/79] Try and fix nullref --- src/Ryujinx.UI.Common/App/ApplicationLibrary.cs | 12 +++++------- src/Ryujinx.UI.Common/Helper/TitleUpdatesHelper.cs | 4 ++-- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index cc5a63ab8..a750db997 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -1082,11 +1082,10 @@ namespace Ryujinx.UI.App.Common var currentlySelected = TitleUpdates.Items.FindFirst(it => it.TitleUpdate.TitleIdBase == update.TitleIdBase && it.IsSelected); - var shouldSelect = !currentlySelected.HasValue || - currentlySelected.Value.TitleUpdate.Version < update.Version; + var shouldSelect = currentlySelected.Check(curr => curr.TitleUpdate.Version < update.Version); _titleUpdates.AddOrUpdate((update, shouldSelect)); - + if (currentlySelected.HasValue && shouldSelect) { _titleUpdates.AddOrUpdate((currentlySelected.Value.TitleUpdate, false)); @@ -1478,7 +1477,7 @@ namespace Ryujinx.UI.App.Common if (TryGetTitleUpdatesFromFile(application.Path, out var bundledUpdates)) { - var savedUpdateLookup = savedUpdates.Select(update => update.Item1).ToHashSet(); + var savedUpdateLookup = savedUpdates.Select(update => update.Update).ToHashSet(); bool updatesChanged = false; foreach (var update in bundledUpdates.OrderByDescending(bundled => bundled.Version)) @@ -1486,11 +1485,10 @@ namespace Ryujinx.UI.App.Common if (!savedUpdateLookup.Contains(update)) { bool shouldSelect = false; - if (!selectedUpdate.HasValue || selectedUpdate.Value.Item1.Version < update.Version) + if (selectedUpdate.Check(su => su.Update.Version < update.Version)) { shouldSelect = true; - if (selectedUpdate) - _titleUpdates.AddOrUpdate((selectedUpdate.Value.Item1, false)); + _titleUpdates.AddOrUpdate((selectedUpdate.Value.Update, false)); selectedUpdate = (update, true); } diff --git a/src/Ryujinx.UI.Common/Helper/TitleUpdatesHelper.cs b/src/Ryujinx.UI.Common/Helper/TitleUpdatesHelper.cs index c6bacfd91..36de8b31a 100644 --- a/src/Ryujinx.UI.Common/Helper/TitleUpdatesHelper.cs +++ b/src/Ryujinx.UI.Common/Helper/TitleUpdatesHelper.cs @@ -28,7 +28,7 @@ namespace Ryujinx.UI.Common.Helper { private static readonly TitleUpdateMetadataJsonSerializerContext _serializerContext = new(JsonHelper.GetDefaultSerializerOptions()); - public static List<(TitleUpdateModel, bool IsSelected)> LoadTitleUpdatesJson(VirtualFileSystem vfs, ulong applicationIdBase) + public static List<(TitleUpdateModel Update, bool IsSelected)> LoadTitleUpdatesJson(VirtualFileSystem vfs, ulong applicationIdBase) { var titleUpdatesJsonPath = PathToGameUpdatesJson(applicationIdBase); @@ -77,7 +77,7 @@ namespace Ryujinx.UI.Common.Helper JsonHelper.SerializeToFile(titleUpdatesJsonPath, titleUpdateWindowData, _serializerContext.TitleUpdateMetadata); } - private static List<(TitleUpdateModel, bool IsSelected)> LoadTitleUpdates(VirtualFileSystem vfs, TitleUpdateMetadata titleUpdateMetadata, ulong applicationIdBase) + private static List<(TitleUpdateModel Update, bool IsSelected)> LoadTitleUpdates(VirtualFileSystem vfs, TitleUpdateMetadata titleUpdateMetadata, ulong applicationIdBase) { var result = new List<(TitleUpdateModel, bool IsSelected)>(); From 315a1819c0f85b6dea7ae971af59b6de64598d75 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 7 Dec 2024 05:31:37 -0600 Subject: [PATCH 13/79] Attempt #2 --- src/Ryujinx.UI.Common/App/ApplicationLibrary.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs index a750db997..cb6467f5e 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationLibrary.cs @@ -1079,10 +1079,12 @@ namespace Ryujinx.UI.App.Common private bool AddAndAutoSelectUpdate(TitleUpdateModel update) { + if (update == null) return false; + var currentlySelected = TitleUpdates.Items.FindFirst(it => it.TitleUpdate.TitleIdBase == update.TitleIdBase && it.IsSelected); - var shouldSelect = currentlySelected.Check(curr => curr.TitleUpdate.Version < update.Version); + var shouldSelect = currentlySelected.Check(curr => curr.TitleUpdate?.Version < update.Version); _titleUpdates.AddOrUpdate((update, shouldSelect)); @@ -1485,7 +1487,7 @@ namespace Ryujinx.UI.App.Common if (!savedUpdateLookup.Contains(update)) { bool shouldSelect = false; - if (selectedUpdate.Check(su => su.Update.Version < update.Version)) + if (selectedUpdate.Check(su => su.Update?.Version < update.Version)) { shouldSelect = true; _titleUpdates.AddOrUpdate((selectedUpdate.Value.Update, false)); From de00a71690d482f14e54d1b1fd21a8d04de84929 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 7 Dec 2024 05:48:11 -0600 Subject: [PATCH 14/79] UI: Fix missing total DLC count. Fixes #347. --- src/Ryujinx/Assets/Locales/ar_SA.json | 2 +- src/Ryujinx/Assets/Locales/de_DE.json | 2 +- src/Ryujinx/Assets/Locales/el_GR.json | 2 +- src/Ryujinx/Assets/Locales/en_US.json | 2 +- src/Ryujinx/Assets/Locales/es_ES.json | 2 +- src/Ryujinx/Assets/Locales/fr_FR.json | 2 +- src/Ryujinx/Assets/Locales/it_IT.json | 2 +- src/Ryujinx/Assets/Locales/ja_JP.json | 2 +- src/Ryujinx/Assets/Locales/ko_KR.json | 2 +- src/Ryujinx/Assets/Locales/pl_PL.json | 2 +- src/Ryujinx/Assets/Locales/pt_BR.json | 2 +- src/Ryujinx/Assets/Locales/ru_RU.json | 2 +- src/Ryujinx/Assets/Locales/th_TH.json | 2 +- src/Ryujinx/Assets/Locales/tr_TR.json | 2 +- src/Ryujinx/Assets/Locales/uk_UA.json | 2 +- src/Ryujinx/Assets/Locales/zh_TW.json | 2 +- .../DownloadableContentManagerWindow.axaml.cs | 14 ++++---------- 17 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/Ryujinx/Assets/Locales/ar_SA.json b/src/Ryujinx/Assets/Locales/ar_SA.json index 412695af6..bdb6a92ec 100644 --- a/src/Ryujinx/Assets/Locales/ar_SA.json +++ b/src/Ryujinx/Assets/Locales/ar_SA.json @@ -788,7 +788,7 @@ "CheatWindowHeading": "الغش متوفر لـ {0} [{1}]", "BuildId": "معرف البناء:", "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", - "DlcWindowHeading": "المحتويات القابلة للتنزيل {0}", + "DlcWindowHeading": "{0} DLC(s) available", "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", diff --git a/src/Ryujinx/Assets/Locales/de_DE.json b/src/Ryujinx/Assets/Locales/de_DE.json index 76e8dfadd..9f4b63a95 100644 --- a/src/Ryujinx/Assets/Locales/de_DE.json +++ b/src/Ryujinx/Assets/Locales/de_DE.json @@ -788,7 +788,7 @@ "CheatWindowHeading": "Cheats verfügbar für {0} [{1}]", "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", "BuildId": "BuildId:", - "DlcWindowHeading": "DLC verfügbar für {0} [{1}]", + "DlcWindowHeading": "{0} DLC(s) available", "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", diff --git a/src/Ryujinx/Assets/Locales/el_GR.json b/src/Ryujinx/Assets/Locales/el_GR.json index 0409297ac..7979b9228 100644 --- a/src/Ryujinx/Assets/Locales/el_GR.json +++ b/src/Ryujinx/Assets/Locales/el_GR.json @@ -788,7 +788,7 @@ "CheatWindowHeading": "Διαθέσιμα Cheats για {0} [{1}]", "BuildId": "BuildId:", "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", - "DlcWindowHeading": "{0} Downloadable Content(s) available for {1} ({2})", + "DlcWindowHeading": "{0} DLC(s) available", "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index ba183c8bd..0598665fd 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -802,7 +802,7 @@ "CheatWindowHeading": "Cheats Available for {0} [{1}]", "BuildId": "BuildId:", "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", - "DlcWindowHeading": "{0} Downloadable Content(s) available for {1} ({2})", + "DlcWindowHeading": "{0} DLC(s) available", "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", diff --git a/src/Ryujinx/Assets/Locales/es_ES.json b/src/Ryujinx/Assets/Locales/es_ES.json index 934031c72..3774605f6 100644 --- a/src/Ryujinx/Assets/Locales/es_ES.json +++ b/src/Ryujinx/Assets/Locales/es_ES.json @@ -787,7 +787,7 @@ "UpdateWindowBundledContentNotice": "Las actualizaciones agrupadas no pueden ser eliminadas, solamente deshabilitadas.", "CheatWindowHeading": "Cheats disponibles para {0} [{1}]", "BuildId": "Id de compilación:", - "DlcWindowHeading": "Contenido descargable disponible para {0} [{1}]", + "DlcWindowHeading": "{0} DLC(s) available", "DlcWindowDlcAddedMessage": "Se agregaron {0} nuevo(s) contenido(s) descargable(s)", "AutoloadDlcAddedMessage": "Se agregaron {0} nuevo(s) contenido(s) descargable(s)", "AutoloadDlcRemovedMessage": "Se eliminaron {0} contenido(s) descargable(s) faltantes", diff --git a/src/Ryujinx/Assets/Locales/fr_FR.json b/src/Ryujinx/Assets/Locales/fr_FR.json index 0223e322e..c5a4bbeec 100644 --- a/src/Ryujinx/Assets/Locales/fr_FR.json +++ b/src/Ryujinx/Assets/Locales/fr_FR.json @@ -788,7 +788,7 @@ "CheatWindowHeading": "Cheats disponibles pour {0} [{1}]", "BuildId": "BuildId :", "DlcWindowBundledContentNotice": "Les DLC inclus avec le jeu ne peuvent pas être supprimés mais peuvent être désactivés.", - "DlcWindowHeading": "{0} Contenu(s) téléchargeable(s)", + "DlcWindowHeading": "{0} DLC(s) available", "DlcWindowDlcAddedMessage": "{0} nouveau(x) contenu(s) téléchargeable(s) ajouté(s)", "AutoloadDlcAddedMessage": "{0} nouveau(x) contenu(s) téléchargeable(s) ajouté(s)", "AutoloadDlcRemovedMessage": "{0} contenu(s) téléchargeable(s) manquant(s) supprimé(s)", diff --git a/src/Ryujinx/Assets/Locales/it_IT.json b/src/Ryujinx/Assets/Locales/it_IT.json index 5ca17bc2e..18e4ee04f 100644 --- a/src/Ryujinx/Assets/Locales/it_IT.json +++ b/src/Ryujinx/Assets/Locales/it_IT.json @@ -788,7 +788,7 @@ "CheatWindowHeading": "Trucchi disponibili per {0} [{1}]", "BuildId": "ID Build", "DlcWindowBundledContentNotice": "i DLC \"impacchettati\" non possono essere rimossi, ma solo disabilitati.", - "DlcWindowHeading": "DLC disponibili per {0} [{1}]", + "DlcWindowHeading": "{0} DLC(s) available", "DlcWindowDlcAddedMessage": "{0} nuovo/i contenuto/i scaricabile/i aggiunto/i", "AutoloadDlcAddedMessage": "{0} contenuto/i scaricabile/i aggiunto/i", "AutoloadDlcRemovedMessage": "{0} contenuto/i scaricabile/i mancante/i rimosso/i", diff --git a/src/Ryujinx/Assets/Locales/ja_JP.json b/src/Ryujinx/Assets/Locales/ja_JP.json index ffa768c13..6ecc74009 100644 --- a/src/Ryujinx/Assets/Locales/ja_JP.json +++ b/src/Ryujinx/Assets/Locales/ja_JP.json @@ -787,7 +787,7 @@ "UpdateWindowBundledContentNotice": "Bundled updates cannot be removed, only disabled.", "CheatWindowHeading": "利用可能なチート {0} [{1}]", "BuildId": "ビルドID:", - "DlcWindowHeading": "利用可能な DLC {0} [{1}]", + "DlcWindowHeading": "{0} DLC(s) available", "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", diff --git a/src/Ryujinx/Assets/Locales/ko_KR.json b/src/Ryujinx/Assets/Locales/ko_KR.json index 8731c8662..71aaa41e8 100644 --- a/src/Ryujinx/Assets/Locales/ko_KR.json +++ b/src/Ryujinx/Assets/Locales/ko_KR.json @@ -788,7 +788,7 @@ "CheatWindowHeading": "{0} [{1}]에 사용 가능한 치트", "BuildId": "빌드ID:", "DlcWindowBundledContentNotice": "번들 DLC는 제거할 수 없으며 비활성화만 가능합니다.", - "DlcWindowHeading": "{1} ({2})에 내려받기 가능한 콘텐츠 {0}개 사용 가능", + "DlcWindowHeading": "{0} DLC(s) available", "DlcWindowDlcAddedMessage": "{0}개의 새로운 내려받기 가능한 콘텐츠가 추가됨", "AutoloadDlcAddedMessage": "{0}개의 새로운 내려받기 가능한 콘텐츠가 추가됨", "AutoloadDlcRemovedMessage": "{0}개의 내려받기 가능한 콘텐츠가 제거됨", diff --git a/src/Ryujinx/Assets/Locales/pl_PL.json b/src/Ryujinx/Assets/Locales/pl_PL.json index d87453ef2..6a2fa08b1 100644 --- a/src/Ryujinx/Assets/Locales/pl_PL.json +++ b/src/Ryujinx/Assets/Locales/pl_PL.json @@ -788,7 +788,7 @@ "CheatWindowHeading": "Kody Dostępne dla {0} [{1}]", "BuildId": "Identyfikator wersji:", "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", - "DlcWindowHeading": "{0} Zawartości do Pobrania dostępna dla {1} ({2})", + "DlcWindowHeading": "{0} DLC(s) available", "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", diff --git a/src/Ryujinx/Assets/Locales/pt_BR.json b/src/Ryujinx/Assets/Locales/pt_BR.json index c240bd804..90b78b732 100644 --- a/src/Ryujinx/Assets/Locales/pt_BR.json +++ b/src/Ryujinx/Assets/Locales/pt_BR.json @@ -787,7 +787,7 @@ "CheatWindowHeading": "Cheats disponíveis para {0} [{1}]", "BuildId": "ID da Build:", "DlcWindowBundledContentNotice": "DLCs incorporadas não podem ser removidas, apenas desativadas.", - "DlcWindowHeading": "{0} DLCs disponíveis para {1} ({2})", + "DlcWindowHeading": "{0} DLC(s) available", "DlcWindowDlcAddedMessage": "{0} novo(s) conteúdo(s) para download adicionado(s)", "AutoloadDlcAddedMessage": "{0} novo(s) conteúdo(s) para download adicionado(s)", "AutoloadDlcRemovedMessage": "{0} conteúdo(s) para download ausente(s) removido(s)", diff --git a/src/Ryujinx/Assets/Locales/ru_RU.json b/src/Ryujinx/Assets/Locales/ru_RU.json index 1046208fb..f058154e9 100644 --- a/src/Ryujinx/Assets/Locales/ru_RU.json +++ b/src/Ryujinx/Assets/Locales/ru_RU.json @@ -788,7 +788,7 @@ "CheatWindowHeading": "Доступные читы для {0} [{1}]", "BuildId": "ID версии:", "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", - "DlcWindowHeading": "{0} DLC", + "DlcWindowHeading": "{0} DLC(s) available", "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", diff --git a/src/Ryujinx/Assets/Locales/th_TH.json b/src/Ryujinx/Assets/Locales/th_TH.json index e29004e10..33b2c4f30 100644 --- a/src/Ryujinx/Assets/Locales/th_TH.json +++ b/src/Ryujinx/Assets/Locales/th_TH.json @@ -788,7 +788,7 @@ "CheatWindowHeading": "สูตรโกงมีให้สำหรับ {0} [{1}]", "BuildId": "รหัสการสร้าง:", "DlcWindowBundledContentNotice": "แพ็ค DLC ไม่สามารถลบทิ้งได้ สามารถปิดใช้งานได้เท่านั้น", - "DlcWindowHeading": "{0} DLC ที่สามารถดาวน์โหลดได้", + "DlcWindowHeading": "{0} DLC(s) available", "DlcWindowDlcAddedMessage": "{0} DLC ใหม่ที่เพิ่มเข้ามา", "AutoloadDlcAddedMessage": "{0} ใหม่ที่เพิ่มเข้ามา", "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", diff --git a/src/Ryujinx/Assets/Locales/tr_TR.json b/src/Ryujinx/Assets/Locales/tr_TR.json index 101206210..72da205cb 100644 --- a/src/Ryujinx/Assets/Locales/tr_TR.json +++ b/src/Ryujinx/Assets/Locales/tr_TR.json @@ -788,7 +788,7 @@ "CheatWindowHeading": "{0} için Hile mevcut [{1}]", "BuildId": "BuildId:", "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", - "DlcWindowHeading": "{0} Downloadable Content(s) available for {1} ({2})", + "DlcWindowHeading": "{0} DLC(s) available", "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", diff --git a/src/Ryujinx/Assets/Locales/uk_UA.json b/src/Ryujinx/Assets/Locales/uk_UA.json index 89e565bf3..06f658640 100644 --- a/src/Ryujinx/Assets/Locales/uk_UA.json +++ b/src/Ryujinx/Assets/Locales/uk_UA.json @@ -788,7 +788,7 @@ "CheatWindowHeading": "Коди доступні для {0} [{1}]", "BuildId": "ID збірки:", "DlcWindowBundledContentNotice": "Bundled DLC cannot be removed, only disabled.", - "DlcWindowHeading": "Вміст для завантаження, доступний для {1} ({2}): {0}", + "DlcWindowHeading": "{0} DLC(s) available", "DlcWindowDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcAddedMessage": "{0} new downloadable content(s) added", "AutoloadDlcRemovedMessage": "{0} missing downloadable content(s) removed", diff --git a/src/Ryujinx/Assets/Locales/zh_TW.json b/src/Ryujinx/Assets/Locales/zh_TW.json index 792ced42b..64f137885 100644 --- a/src/Ryujinx/Assets/Locales/zh_TW.json +++ b/src/Ryujinx/Assets/Locales/zh_TW.json @@ -788,7 +788,7 @@ "CheatWindowHeading": "可用於 {0} [{1}] 的密技", "BuildId": "組建識別碼:", "DlcWindowBundledContentNotice": "附帶的 DLC 只能被停用而無法被刪除。", - "DlcWindowHeading": "{0} 個可下載內容", + "DlcWindowHeading": "{0} DLC(s) available", "DlcWindowDlcAddedMessage": "已加入 {0} 個 DLC", "AutoloadDlcAddedMessage": "已加入 {0} 個 DLC", "AutoloadDlcRemovedMessage": "已刪除 {0} 個遺失的 DLC", diff --git a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml.cs b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml.cs index 340515a5b..2afa8b529 100644 --- a/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/DownloadableContentManagerWindow.axaml.cs @@ -61,23 +61,17 @@ namespace Ryujinx.Ava.UI.Windows private void RemoveDLC(object sender, RoutedEventArgs e) { - if (sender is Button button) + if (sender is Button { DataContext: DownloadableContentModel dlc }) { - if (button.DataContext is DownloadableContentModel model) - { - ViewModel.Remove(model); - } + ViewModel.Remove(dlc); } } private void OpenLocation(object sender, RoutedEventArgs e) { - if (sender is Button button) + if (sender is Button { DataContext: DownloadableContentModel dlc }) { - if (button.DataContext is DownloadableContentModel model) - { - OpenHelper.LocateFile(model.ContainerPath); - } + OpenHelper.LocateFile(dlc.ContainerPath); } } From 06abba25c1f63737e5bccbbabbc71a6ade33c366 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 7 Dec 2024 06:22:46 -0600 Subject: [PATCH 15/79] UI: Adapt accent color to the user's system. https://amwx.github.io/FluentAvaloniaDocs/pages/FATheme/Accents#using-the-systems-accent-color --- src/Ryujinx/App.axaml | 2 +- src/Ryujinx/Assets/Styles/Themes.xaml | 36 +------------------ .../Common/Markup/BasicMarkupExtension.cs | 4 +-- src/Ryujinx/Common/Markup/MarkupExtensions.cs | 6 ++-- .../UI/Controls/ApplicationGridView.axaml | 2 +- .../UI/Controls/ApplicationListView.axaml | 2 +- 6 files changed, 9 insertions(+), 43 deletions(-) diff --git a/src/Ryujinx/App.axaml b/src/Ryujinx/App.axaml index 5a603509c..5c96f97f2 100644 --- a/src/Ryujinx/App.axaml +++ b/src/Ryujinx/App.axaml @@ -11,7 +11,7 @@ - + diff --git a/src/Ryujinx/Assets/Styles/Themes.xaml b/src/Ryujinx/Assets/Styles/Themes.xaml index 056eba228..46e298035 100644 --- a/src/Ryujinx/Assets/Styles/Themes.xaml +++ b/src/Ryujinx/Assets/Styles/Themes.xaml @@ -4,18 +4,6 @@ - - - #FF00C3E3 - #FF00C3E3 - #FF00C3E3 - #FF00C3E3 - #FF00C3E3 - #FF00C3E3 - #FF00C3E3 - #FFe8e8e8 #FF00FABB #FFF0F0F0 #FFd6d6d6 @@ -26,6 +14,7 @@ #b3ffffff #80cccccc #A0000000 + #fffcd12a #FF2EEAC9 #FFFF4554 #6483F5 @@ -33,18 +22,6 @@ - - - #FF00C3E3 - #FF00C3E3 - #FF00C3E3 - #FF00C3E3 - #FF00C3E3 - #FF00C3E3 - #FF00C3E3 - #FFe8e8e8 #FF00FABB #FFF0F0F0 #FFd6d6d6 @@ -59,18 +36,7 @@ - - #008AA8 - #FF00C3E3 - #FF99b000 - #FF006d7d - #FF00525E - #FF00dbff - #FF19dfff - #FF33e3ff #FF00FABB #FF2D2D2D #FF505050 diff --git a/src/Ryujinx/Common/Markup/BasicMarkupExtension.cs b/src/Ryujinx/Common/Markup/BasicMarkupExtension.cs index 67c016562..b1b7361a6 100644 --- a/src/Ryujinx/Common/Markup/BasicMarkupExtension.cs +++ b/src/Ryujinx/Common/Markup/BasicMarkupExtension.cs @@ -17,13 +17,13 @@ namespace Ryujinx.Ava.Common.Markup public virtual string Name => "Item"; public virtual Action? Setter => null; - protected abstract T? GetValue(); + protected abstract T? Value { get; } protected virtual void ConfigureBindingExtension(CompiledBindingExtension _) { } private ClrPropertyInfo PropertyInfo => new(Name, - _ => GetValue(), + _ => Value, Setter as Action, typeof(T)); diff --git a/src/Ryujinx/Common/Markup/MarkupExtensions.cs b/src/Ryujinx/Common/Markup/MarkupExtensions.cs index a804792c7..cae6d8c2c 100644 --- a/src/Ryujinx/Common/Markup/MarkupExtensions.cs +++ b/src/Ryujinx/Common/Markup/MarkupExtensions.cs @@ -6,17 +6,17 @@ namespace Ryujinx.Ava.Common.Markup { internal class IconExtension(string iconString) : BasicMarkupExtension { - protected override Icon GetValue() => new() { Value = iconString }; + protected override Icon Value => new() { Value = iconString }; } internal class SpinningIconExtension(string iconString) : BasicMarkupExtension { - protected override Icon GetValue() => new() { Value = iconString, Animation = IconAnimation.Spin }; + protected override Icon Value => new() { Value = iconString, Animation = IconAnimation.Spin }; } internal class LocaleExtension(LocaleKeys key) : BasicMarkupExtension { - protected override string GetValue() => LocaleManager.Instance[key]; + protected override string Value => LocaleManager.Instance[key]; protected override void ConfigureBindingExtension(CompiledBindingExtension bindingExtension) => bindingExtension.Source = LocaleManager.Instance; diff --git a/src/Ryujinx/UI/Controls/ApplicationGridView.axaml b/src/Ryujinx/UI/Controls/ApplicationGridView.axaml index 98a1c004b..3bcb468ae 100644 --- a/src/Ryujinx/UI/Controls/ApplicationGridView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationGridView.axaml @@ -91,7 +91,7 @@ HorizontalAlignment="Left" VerticalAlignment="Top" FontSize="16" - Foreground="{DynamicResource SystemAccentColor}" + Foreground="{DynamicResource FavoriteApplicationIconColor}" IsVisible="{Binding Favorite}" Symbol="StarFilled" /> diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml b/src/Ryujinx/UI/Controls/ApplicationListView.axaml index 0daa77ac4..8a72ebfbf 100644 --- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml @@ -146,7 +146,7 @@ HorizontalAlignment="Left" VerticalAlignment="Top" FontSize="16" - Foreground="{DynamicResource SystemAccentColor}" + Foreground="{DynamicResource FavoriteApplicationIconColor}" IsVisible="{Binding Favorite}" Symbol="StarFilled" /> From 8ae72c1a0002a3c1bae7d92fad125333eb6e3edb Mon Sep 17 00:00:00 2001 From: bangfire <168100143+bangfire@users.noreply.github.com> Date: Sat, 7 Dec 2024 13:17:39 +0000 Subject: [PATCH 16/79] Fix Windows Terminal hide/show functions (#342) https://stackoverflow.com/a/78577080 --- src/Ryujinx.UI.Common/Helper/ConsoleHelper.cs | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/Ryujinx.UI.Common/Helper/ConsoleHelper.cs b/src/Ryujinx.UI.Common/Helper/ConsoleHelper.cs index 623952b37..99b209c6e 100644 --- a/src/Ryujinx.UI.Common/Helper/ConsoleHelper.cs +++ b/src/Ryujinx.UI.Common/Helper/ConsoleHelper.cs @@ -7,6 +7,24 @@ namespace Ryujinx.UI.Common.Helper { public static partial class ConsoleHelper { + [SupportedOSPlatform("windows")] + [LibraryImport("kernel32")] + private static partial nint GetConsoleWindow(); + + [SupportedOSPlatform("windows")] + [LibraryImport("user32")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool ShowWindow(nint hWnd, int nCmdShow); + + [SupportedOSPlatform("windows")] + [LibraryImport("user32")] + private static partial nint GetForegroundWindow(); + + [SupportedOSPlatform("windows")] + [LibraryImport("user32")] + [return: MarshalAs(UnmanagedType.Bool)] + private static partial bool SetForegroundWindow(nint hWnd); + public static bool SetConsoleWindowStateSupported => OperatingSystem.IsWindows(); public static void SetConsoleWindowState(bool show) @@ -35,16 +53,11 @@ namespace Ryujinx.UI.Common.Helper return; } + SetForegroundWindow(hWnd); + + hWnd = GetForegroundWindow(); + ShowWindow(hWnd, show ? SW_SHOW : SW_HIDE); } - - [SupportedOSPlatform("windows")] - [LibraryImport("kernel32")] - private static partial nint GetConsoleWindow(); - - [SupportedOSPlatform("windows")] - [LibraryImport("user32")] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool ShowWindow(nint hWnd, int nCmdShow); } } From ec11bf2af9087a9ec834252fb74bf8df5365efd0 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 7 Dec 2024 08:53:23 -0600 Subject: [PATCH 17/79] i18n: Clean out old translations and reset outdated ones --- src/Ryujinx/Assets/Locales/ar_SA.json | 6 ++---- src/Ryujinx/Assets/Locales/de_DE.json | 6 ++---- src/Ryujinx/Assets/Locales/el_GR.json | 6 ++---- src/Ryujinx/Assets/Locales/en_US.json | 6 ++---- src/Ryujinx/Assets/Locales/es_ES.json | 6 ++---- src/Ryujinx/Assets/Locales/fr_FR.json | 6 ++---- src/Ryujinx/Assets/Locales/he_IL.json | 6 ++---- src/Ryujinx/Assets/Locales/it_IT.json | 6 ++---- src/Ryujinx/Assets/Locales/ja_JP.json | 6 ++---- src/Ryujinx/Assets/Locales/ko_KR.json | 6 ++---- src/Ryujinx/Assets/Locales/pl_PL.json | 6 ++---- src/Ryujinx/Assets/Locales/pt_BR.json | 6 ++---- src/Ryujinx/Assets/Locales/ru_RU.json | 6 ++---- src/Ryujinx/Assets/Locales/th_TH.json | 6 ++---- src/Ryujinx/Assets/Locales/tr_TR.json | 6 ++---- src/Ryujinx/Assets/Locales/uk_UA.json | 6 ++---- src/Ryujinx/Assets/Locales/zh_CN.json | 6 ++---- src/Ryujinx/Assets/Locales/zh_TW.json | 6 ++---- 18 files changed, 36 insertions(+), 72 deletions(-) diff --git a/src/Ryujinx/Assets/Locales/ar_SA.json b/src/Ryujinx/Assets/Locales/ar_SA.json index bdb6a92ec..fee84779a 100644 --- a/src/Ryujinx/Assets/Locales/ar_SA.json +++ b/src/Ryujinx/Assets/Locales/ar_SA.json @@ -552,15 +552,13 @@ "AboutUrlTooltipMessage": "انقر لفتح موقع ريوجينكس في متصفحك الافتراضي.", "AboutDisclaimerMessage": "ريوجينكس لا ينتمي إلى نينتندو™،\nأو أي من شركائها بأي شكل من الأشكال.", "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) يتم \nاستخدامه في محاكاة أمبيو لدينا.", - "AboutPatreonUrlTooltipMessage": "انقر لفتح صفحة ريوجينكس في باتريون في متصفحك الافتراضي.", "AboutGithubUrlTooltipMessage": "انقر لفتح صفحة ريوجينكس في غيت هاب في متصفحك الافتراضي.", "AboutDiscordUrlTooltipMessage": "انقر لفتح دعوة إلى خادم ريوجينكس في ديكسورد في متصفحك الافتراضي.", - "AboutTwitterUrlTooltipMessage": "انقر لفتح صفحة ريوجينكس في تويتر في متصفحك الافتراضي.", "AboutRyujinxAboutTitle": "حول:", - "AboutRyujinxAboutContent": "ريوجينكس هو محاكي لجهاز نينتندو سويتش™.\nمن فضلك ادعمنا على باتريون.\nاحصل على آخر الأخبار على تويتر أو ديسكورد.\nيمكن للمطورين المهتمين بالمساهمة معرفة المزيد على غيت هاب أو ديسكورد.", + "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", "AboutRyujinxMaintainersTitle": "تتم صيانته بواسطة:", + "AboutRyujinxFormerMaintainersTitle": "Formerly Maintained By:", "AboutRyujinxMaintainersContentTooltipMessage": "انقر لفتح صفحة المساهمين في متصفحك الافتراضي.", - "AboutRyujinxSupprtersTitle": "مدعوم على باتريون بواسطة:", "AmiiboSeriesLabel": "مجموعة أميبو", "AmiiboCharacterLabel": "شخصية", "AmiiboScanButtonLabel": "فحصه", diff --git a/src/Ryujinx/Assets/Locales/de_DE.json b/src/Ryujinx/Assets/Locales/de_DE.json index 9f4b63a95..5391df474 100644 --- a/src/Ryujinx/Assets/Locales/de_DE.json +++ b/src/Ryujinx/Assets/Locales/de_DE.json @@ -552,15 +552,13 @@ "AboutUrlTooltipMessage": "Klicke hier, um die Ryujinx Website im Standardbrowser zu öffnen.", "AboutDisclaimerMessage": "Ryujinx ist in keinster Weise weder mit Nintendo™, \nnoch mit deren Partnern verbunden.", "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) wird in unserer Amiibo \nEmulation benutzt.", - "AboutPatreonUrlTooltipMessage": "Klicke hier, um die Ryujinx Patreon Seite im Standardbrowser zu öffnen.", "AboutGithubUrlTooltipMessage": "Klicke hier, um die Ryujinx GitHub Seite im Standardbrowser zu öffnen.", "AboutDiscordUrlTooltipMessage": "Klicke hier, um eine Einladung zum Ryujinx Discord Server im Standardbrowser zu öffnen.", - "AboutTwitterUrlTooltipMessage": "Klicke hier, um die Ryujinx Twitter Seite im Standardbrowser zu öffnen.", "AboutRyujinxAboutTitle": "Über:", - "AboutRyujinxAboutContent": "Ryujinx ist ein Nintendo Switch™ Emulator.\nBitte unterstütze uns auf Patreon.\nAuf Twitter oder Discord erfährst du alle Neuigkeiten.\nEntwickler, die an einer Mitarbeit interessiert sind, können auf GitHub oder Discord mehr erfahren.", + "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", "AboutRyujinxMaintainersTitle": "Entwickelt von:", + "AboutRyujinxFormerMaintainersTitle": "Formerly Maintained By:", "AboutRyujinxMaintainersContentTooltipMessage": "Klicke hier, um die Liste der Mitwirkenden im Standardbrowser zu öffnen.", - "AboutRyujinxSupprtersTitle": "Unterstützt auf Patreon von:", "AmiiboSeriesLabel": "Amiibo-Serie", "AmiiboCharacterLabel": "Charakter", "AmiiboScanButtonLabel": "Einscannen", diff --git a/src/Ryujinx/Assets/Locales/el_GR.json b/src/Ryujinx/Assets/Locales/el_GR.json index 7979b9228..3c308ac84 100644 --- a/src/Ryujinx/Assets/Locales/el_GR.json +++ b/src/Ryujinx/Assets/Locales/el_GR.json @@ -552,15 +552,13 @@ "AboutUrlTooltipMessage": "Κάντε κλικ για να ανοίξετε τον ιστότοπο Ryujinx στο προεπιλεγμένο πρόγραμμα περιήγησης.", "AboutDisclaimerMessage": "Το Ryujinx δεν είναι συνδεδεμένο με τη Nintendo™,\nούτε με κανέναν από τους συνεργάτες της, με οποιονδήποτε τρόπο.", "AboutAmiiboDisclaimerMessage": "Το AmiiboAPI (www.amiiboapi.com) χρησιμοποιείται\nστην προσομοίωση Amiibo.", - "AboutPatreonUrlTooltipMessage": "Κάντε κλικ για να ανοίξετε τη σελίδα Ryujinx Patreon στο προεπιλεγμένο πρόγραμμα περιήγησης.", "AboutGithubUrlTooltipMessage": "Κάντε κλικ για να ανοίξετε τη σελίδα Ryujinx GitHub στο προεπιλεγμένο πρόγραμμα περιήγησης.", "AboutDiscordUrlTooltipMessage": "Κάντε κλικ για να ανοίξετε μία πρόσκληση στον διακομιστή Ryujinx Discord στο προεπιλεγμένο πρόγραμμα περιήγησης.", - "AboutTwitterUrlTooltipMessage": "Κάντε κλικ για να ανοίξετε τη σελίδα Ryujinx Twitter στο προεπιλεγμένο πρόγραμμα περιήγησης.", "AboutRyujinxAboutTitle": "Σχετικά με:", - "AboutRyujinxAboutContent": "Το Ryujinx είναι ένας εξομοιωτής για το Nintendo Switch™.\nΥποστηρίξτε μας στο Patreon.\nΛάβετε όλα τα τελευταία νέα στο Twitter ή στο Discord.\nΟι προγραμματιστές που ενδιαφέρονται να συνεισφέρουν μπορούν να μάθουν περισσότερα στο GitHub ή στο Discord μας.", + "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", "AboutRyujinxMaintainersTitle": "Συντηρείται από:", + "AboutRyujinxFormerMaintainersTitle": "Formerly Maintained By:", "AboutRyujinxMaintainersContentTooltipMessage": "Κάντε κλικ για να ανοίξετε τη σελίδα Συνεισφέροντες στο προεπιλεγμένο πρόγραμμα περιήγησης.", - "AboutRyujinxSupprtersTitle": "Υποστηρίζεται στο Patreon από:", "AmiiboSeriesLabel": "Σειρά Amiibo", "AmiiboCharacterLabel": "Χαρακτήρας", "AmiiboScanButtonLabel": "Σαρώστε το", diff --git a/src/Ryujinx/Assets/Locales/en_US.json b/src/Ryujinx/Assets/Locales/en_US.json index 0598665fd..bad69a41c 100644 --- a/src/Ryujinx/Assets/Locales/en_US.json +++ b/src/Ryujinx/Assets/Locales/en_US.json @@ -564,15 +564,13 @@ "AboutUrlTooltipMessage": "Click to open the Ryujinx website in your default browser.", "AboutDisclaimerMessage": "Ryujinx is not affiliated with Nintendo™,\nor any of its partners, in any way.", "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) is used\nin our Amiibo emulation.", - "AboutPatreonUrlTooltipMessage": "Click to open the Ryujinx Patreon page in your default browser.", "AboutGithubUrlTooltipMessage": "Click to open the Ryujinx GitHub page in your default browser.", "AboutDiscordUrlTooltipMessage": "Click to open an invite to the Ryujinx Discord server in your default browser.", - "AboutTwitterUrlTooltipMessage": "Click to open the Ryujinx Twitter page in your default browser.", "AboutRyujinxAboutTitle": "About:", - "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nPlease support us on Patreon.\nGet all the latest news on our Twitter or Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", + "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", "AboutRyujinxMaintainersTitle": "Maintained By:", + "AboutRyujinxFormerMaintainersTitle": "Formerly Maintained By:", "AboutRyujinxMaintainersContentTooltipMessage": "Click to open the Contributors page in your default browser.", - "AboutRyujinxSupprtersTitle": "Supported on Patreon By:", "AmiiboSeriesLabel": "Amiibo Series", "AmiiboCharacterLabel": "Character", "AmiiboScanButtonLabel": "Scan It", diff --git a/src/Ryujinx/Assets/Locales/es_ES.json b/src/Ryujinx/Assets/Locales/es_ES.json index 3774605f6..fe8b57f78 100644 --- a/src/Ryujinx/Assets/Locales/es_ES.json +++ b/src/Ryujinx/Assets/Locales/es_ES.json @@ -552,15 +552,13 @@ "AboutUrlTooltipMessage": "Haz clic para abrir el sitio web de Ryujinx en tu navegador predeterminado.", "AboutDisclaimerMessage": "Ryujinx no tiene afiliación alguna con Nintendo™,\nni con ninguno de sus socios.", "AboutAmiiboDisclaimerMessage": "Utilizamos AmiiboAPI (www.amiiboapi.com)\nen nuestra emulación de Amiibo.", - "AboutPatreonUrlTooltipMessage": "Haz clic para abrir el Patreon de Ryujinx en tu navegador predeterminado.", "AboutGithubUrlTooltipMessage": "Haz clic para abrir el GitHub de Ryujinx en tu navegador predeterminado.", "AboutDiscordUrlTooltipMessage": "Haz clic para recibir una invitación al Discord de Ryujinx en tu navegador predeterminado.", - "AboutTwitterUrlTooltipMessage": "Haz clic para abrir el Twitter de Ryujinx en tu navegador predeterminado.", "AboutRyujinxAboutTitle": "Acerca de:", - "AboutRyujinxAboutContent": "Ryujinx es un emulador para Nintendo Switch™.\nPor favor, apóyanos en Patreon.\nEncuentra las noticias más recientes en nuestro Twitter o Discord.\nDesarrolladores interesados en contribuir pueden encontrar más información en GitHub o Discord.", + "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", "AboutRyujinxMaintainersTitle": "Mantenido por:", + "AboutRyujinxFormerMaintainersTitle": "Formerly Maintained By:", "AboutRyujinxMaintainersContentTooltipMessage": "Haz clic para abrir la página de contribuidores en tu navegador predeterminado.", - "AboutRyujinxSupprtersTitle": "Apoyado en Patreon Por:", "AmiiboSeriesLabel": "Serie de Amiibo", "AmiiboCharacterLabel": "Personaje", "AmiiboScanButtonLabel": "Escanear", diff --git a/src/Ryujinx/Assets/Locales/fr_FR.json b/src/Ryujinx/Assets/Locales/fr_FR.json index c5a4bbeec..cd4e74f04 100644 --- a/src/Ryujinx/Assets/Locales/fr_FR.json +++ b/src/Ryujinx/Assets/Locales/fr_FR.json @@ -552,15 +552,13 @@ "AboutUrlTooltipMessage": "Cliquez pour ouvrir le site de Ryujinx dans votre navigateur par défaut.", "AboutDisclaimerMessage": "Ryujinx n'est pas affilié à Nintendo™,\nou à aucun de ses partenaires, de quelque manière que ce soit.", "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) est utilisé\ndans notre émulation Amiibo.", - "AboutPatreonUrlTooltipMessage": "Cliquez pour ouvrir la page Patreon de Ryujinx dans votre navigateur par défaut.", "AboutGithubUrlTooltipMessage": "Cliquez pour ouvrir la page GitHub de Ryujinx dans votre navigateur par défaut.", "AboutDiscordUrlTooltipMessage": "Cliquez pour ouvrir une invitation au serveur Discord de Ryujinx dans votre navigateur par défaut.", - "AboutTwitterUrlTooltipMessage": "Cliquez pour ouvrir la page Twitter de Ryujinx dans votre navigateur par défaut.", "AboutRyujinxAboutTitle": "À propos :", - "AboutRyujinxAboutContent": "Ryujinx est un émulateur pour la Nintendo Switch™.\nMerci de nous soutenir sur Patreon.\nObtenez toutes les dernières actualités sur notre Twitter ou notre Discord.\nLes développeurs intéressés à contribuer peuvent en savoir plus sur notre GitHub ou notre Discord.", + "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", "AboutRyujinxMaintainersTitle": "Maintenu par :", + "AboutRyujinxFormerMaintainersTitle": "Formerly Maintained By:", "AboutRyujinxMaintainersContentTooltipMessage": "Cliquez pour ouvrir la page Contributeurs dans votre navigateur par défaut.", - "AboutRyujinxSupprtersTitle": "Supporté sur Patreon par :", "AmiiboSeriesLabel": "Séries Amiibo", "AmiiboCharacterLabel": "Personnage", "AmiiboScanButtonLabel": "Scanner", diff --git a/src/Ryujinx/Assets/Locales/he_IL.json b/src/Ryujinx/Assets/Locales/he_IL.json index 318068bf3..14cfc4977 100644 --- a/src/Ryujinx/Assets/Locales/he_IL.json +++ b/src/Ryujinx/Assets/Locales/he_IL.json @@ -552,15 +552,13 @@ "AboutUrlTooltipMessage": "לחץ כדי לפתוח את אתר ריוג'ינקס בדפדפן ברירת המחדל שלך.", "AboutDisclaimerMessage": "ריוג'ינקס אינה מזוהת עם נינטנדו,\nאו שוטפייה בכל דרך שהיא.", "AboutAmiiboDisclaimerMessage": "ממשק אמיבו (www.amiiboapi.com) משומש בהדמיית האמיבו שלנו.", - "AboutPatreonUrlTooltipMessage": "לחץ כדי לפתוח את דף הפטראון של ריוג'ינקס בדפדפן ברירת המחדל שלך.", "AboutGithubUrlTooltipMessage": "לחץ כדי לפתוח את דף הגיטהב של ריוג'ינקס בדפדפן ברירת המחדל שלך.", "AboutDiscordUrlTooltipMessage": "לחץ כדי לפתוח הזמנה לשרת הדיסקורד של ריוג'ינקס בדפדפן ברירת המחדל שלך.", - "AboutTwitterUrlTooltipMessage": "לחץ כדי לפתוח את דף הטוויטר של Ryujinx בדפדפן ברירת המחדל שלך.", "AboutRyujinxAboutTitle": "אודות:", - "AboutRyujinxAboutContent": "ריוג'ינקס הוא אמולטור עבור הנינטנדו סוויץ' (כל הזכויות שמורות).\nבבקשה תתמכו בנו בפטראון.\nקבל את כל החדשות האחרונות בטוויטר או בדיסקורד שלנו.\nמפתחים המעוניינים לתרום יכולים לקבל מידע נוסף ב-גיטהאב או ב-דיסקורד שלנו.", + "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", "AboutRyujinxMaintainersTitle": "מתוחזק על ידי:", + "AboutRyujinxFormerMaintainersTitle": "Formerly Maintained By:", "AboutRyujinxMaintainersContentTooltipMessage": "לחץ כדי לפתוח את דף התורמים בדפדפן ברירת המחדל שלך.", - "AboutRyujinxSupprtersTitle": "תמוך באמצעות Patreon", "AmiiboSeriesLabel": "סדרת אמיבו", "AmiiboCharacterLabel": "דמות", "AmiiboScanButtonLabel": "סרוק את זה", diff --git a/src/Ryujinx/Assets/Locales/it_IT.json b/src/Ryujinx/Assets/Locales/it_IT.json index 18e4ee04f..854b831c1 100644 --- a/src/Ryujinx/Assets/Locales/it_IT.json +++ b/src/Ryujinx/Assets/Locales/it_IT.json @@ -552,15 +552,13 @@ "AboutUrlTooltipMessage": "Clicca per aprire il sito web di Ryujinx nel tuo browser predefinito.", "AboutDisclaimerMessage": "Ryujinx non è affiliato con Nintendo™,\no i suoi partner, in alcun modo.", "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) è usata\nnella nostra emulazione Amiibo.", - "AboutPatreonUrlTooltipMessage": "Clicca per aprire la pagina Patreon di Ryujinx nel tuo browser predefinito.", "AboutGithubUrlTooltipMessage": "Clicca per aprire la pagina GitHub di Ryujinx nel tuo browser predefinito.", "AboutDiscordUrlTooltipMessage": "Clicca per aprire un invito al server Discord di Ryujinx nel tuo browser predefinito.", - "AboutTwitterUrlTooltipMessage": "Clicca per aprire la pagina Twitter di Ryujinx nel tuo browser predefinito.", "AboutRyujinxAboutTitle": "Informazioni:", - "AboutRyujinxAboutContent": "Ryujinx è un emulatore per la console Nintendo Switch™.\nSostienici su Patreon.\nRicevi tutte le ultime notizie sul nostro Twitter o su Discord.\nGli sviluppatori interessati a contribuire possono trovare più informazioni sul nostro GitHub o Discord.", + "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", "AboutRyujinxMaintainersTitle": "Mantenuto da:", + "AboutRyujinxFormerMaintainersTitle": "Formerly Maintained By:", "AboutRyujinxMaintainersContentTooltipMessage": "Clicca per aprire la pagina dei contributori nel tuo browser predefinito.", - "AboutRyujinxSupprtersTitle": "Supportato su Patreon da:", "AmiiboSeriesLabel": "Serie Amiibo", "AmiiboCharacterLabel": "Personaggio", "AmiiboScanButtonLabel": "Scansiona", diff --git a/src/Ryujinx/Assets/Locales/ja_JP.json b/src/Ryujinx/Assets/Locales/ja_JP.json index 6ecc74009..19236c21b 100644 --- a/src/Ryujinx/Assets/Locales/ja_JP.json +++ b/src/Ryujinx/Assets/Locales/ja_JP.json @@ -552,15 +552,13 @@ "AboutUrlTooltipMessage": "クリックするとデフォルトのブラウザで Ryujinx のウェブサイトを開きます.", "AboutDisclaimerMessage": "Ryujinx は Nintendo™ および\nそのパートナー企業とは一切関係ありません.", "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) は\nAmiibo エミュレーションに使用されています.", - "AboutPatreonUrlTooltipMessage": "クリックするとデフォルトのブラウザで Ryujinx の Patreon ページを開きます.", "AboutGithubUrlTooltipMessage": "クリックするとデフォルトのブラウザで Ryujinx の Github ページを開きます.", "AboutDiscordUrlTooltipMessage": "クリックするとデフォルトのブラウザで Ryujinx の Discord サーバを開きます.", - "AboutTwitterUrlTooltipMessage": "クリックするとデフォルトのブラウザで Ryujinx の Twitter ページを開きます.", "AboutRyujinxAboutTitle": "Ryujinx について:", - "AboutRyujinxAboutContent": "Ryujinx は Nintendo Switch™ のエミュレータです.\nPatreon で私達の活動を支援してください.\n最新の情報は Twitter または Discord から取得できます.\n貢献したい開発者の方は GitHub または Discord で詳細をご確認ください.", + "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", "AboutRyujinxMaintainersTitle": "開発者:", + "AboutRyujinxFormerMaintainersTitle": "Formerly Maintained By:", "AboutRyujinxMaintainersContentTooltipMessage": "クリックするとデフォルトのブラウザで 貢献者のページを開きます.", - "AboutRyujinxSupprtersTitle": "Patreon での支援者:", "AmiiboSeriesLabel": "Amiibo シリーズ", "AmiiboCharacterLabel": "キャラクタ", "AmiiboScanButtonLabel": "スキャン", diff --git a/src/Ryujinx/Assets/Locales/ko_KR.json b/src/Ryujinx/Assets/Locales/ko_KR.json index 71aaa41e8..8ae7a022a 100644 --- a/src/Ryujinx/Assets/Locales/ko_KR.json +++ b/src/Ryujinx/Assets/Locales/ko_KR.json @@ -552,15 +552,13 @@ "AboutUrlTooltipMessage": "클릭하면 기본 브라우저에서 Ryujinx 웹사이트가 열립니다.", "AboutDisclaimerMessage": "Ryujinx는 Nintendo™\n또는 그 파트너와 제휴한 바가 없습니다.", "AboutAmiiboDisclaimerMessage": "AmiiboAPI(www.amiiboapi.com)는\nAmiibo 에뮬레이션에 사용됩니다.", - "AboutPatreonUrlTooltipMessage": "클릭하면 기본 브라우저에서 Ryujinx Patreon 페이지가 열립니다.", "AboutGithubUrlTooltipMessage": "클릭하면 기본 브라우저에서 Ryujinx GitHub 페이지가 열립니다.", "AboutDiscordUrlTooltipMessage": "클릭하면 기본 브라우저에서 Ryujinx 디스코드 서버 초대장이 열립니다.", - "AboutTwitterUrlTooltipMessage": "클릭하면 기본 브라우저에서 Ryujinx 트위터 페이지가 열립니다.", "AboutRyujinxAboutTitle": "정보 :", - "AboutRyujinxAboutContent": "Ryujinx는 Nintendo Switch™용 에뮬레이터입니다.\nPatreon에서 저희를 후원해 주세요.\nTwitter나 Discord에서 최신 뉴스를 모두 받아보세요.\n기여에 관심이 있는 개발자는 GitHub이나 Discord에서 자세한 내용을 알아볼 수 있습니다.", + "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", "AboutRyujinxMaintainersTitle": "유지 관리 :", + "AboutRyujinxFormerMaintainersTitle": "Formerly Maintained By:", "AboutRyujinxMaintainersContentTooltipMessage": "클릭하면 기본 브라우저에서 기여자 페이지가 열립니다.", - "AboutRyujinxSupprtersTitle": "Patreon에서 후원 :", "AmiiboSeriesLabel": "Amiibo 시리즈", "AmiiboCharacterLabel": "캐릭터", "AmiiboScanButtonLabel": "스캔하기", diff --git a/src/Ryujinx/Assets/Locales/pl_PL.json b/src/Ryujinx/Assets/Locales/pl_PL.json index 6a2fa08b1..7a0b16f6f 100644 --- a/src/Ryujinx/Assets/Locales/pl_PL.json +++ b/src/Ryujinx/Assets/Locales/pl_PL.json @@ -552,15 +552,13 @@ "AboutUrlTooltipMessage": "Kliknij, aby otworzyć stronę Ryujinx w domyślnej przeglądarce.", "AboutDisclaimerMessage": "Ryujinx nie jest w żaden sposób powiązany z Nintendo™,\nani z żadnym z jej partnerów.", "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) jest używane\nw naszej emulacji Amiibo.", - "AboutPatreonUrlTooltipMessage": "Kliknij, aby otworzyć stronę Patreon Ryujinx w domyślnej przeglądarce.", "AboutGithubUrlTooltipMessage": "Kliknij, aby otworzyć stronę GitHub Ryujinx w domyślnej przeglądarce.", "AboutDiscordUrlTooltipMessage": "Kliknij, aby otworzyć zaproszenie na serwer Discord Ryujinx w domyślnej przeglądarce.", - "AboutTwitterUrlTooltipMessage": "Kliknij, aby otworzyć stronę Twitter Ryujinx w domyślnej przeglądarce.", "AboutRyujinxAboutTitle": "O Aplikacji:", - "AboutRyujinxAboutContent": "Ryujinx to emulator Nintendo Switch™.\nWspieraj nas na Patreonie.\nOtrzymuj najnowsze wiadomości na naszym Twitterze lub Discordzie.\nDeweloperzy zainteresowani współpracą mogą dowiedzieć się więcej na naszym GitHubie lub Discordzie.", + "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", "AboutRyujinxMaintainersTitle": "Utrzymywany Przez:", + "AboutRyujinxFormerMaintainersTitle": "Formerly Maintained By:", "AboutRyujinxMaintainersContentTooltipMessage": "Kliknij, aby otworzyć stronę Współtwórcy w domyślnej przeglądarce.", - "AboutRyujinxSupprtersTitle": "Wspierani na Patreonie Przez:", "AmiiboSeriesLabel": "Seria Amiibo", "AmiiboCharacterLabel": "Postać", "AmiiboScanButtonLabel": "Zeskanuj", diff --git a/src/Ryujinx/Assets/Locales/pt_BR.json b/src/Ryujinx/Assets/Locales/pt_BR.json index 90b78b732..acb063aea 100644 --- a/src/Ryujinx/Assets/Locales/pt_BR.json +++ b/src/Ryujinx/Assets/Locales/pt_BR.json @@ -552,15 +552,13 @@ "AboutUrlTooltipMessage": "Clique para abrir o site do Ryujinx no seu navegador padrão.", "AboutDisclaimerMessage": "Ryujinx não é afiliado com a Nintendo™,\nou qualquer um de seus parceiros, de nenhum modo.", "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) é usado\nem nossa emulação de Amiibo.", - "AboutPatreonUrlTooltipMessage": "Clique para abrir a página do Patreon do Ryujinx no seu navegador padrão.", "AboutGithubUrlTooltipMessage": "Clique para abrir a página do GitHub do Ryujinx no seu navegador padrão.", "AboutDiscordUrlTooltipMessage": "Clique para abrir um convite ao servidor do Discord do Ryujinx no seu navegador padrão.", - "AboutTwitterUrlTooltipMessage": "Clique para abrir a página do Twitter do Ryujinx no seu navegador padrão.", "AboutRyujinxAboutTitle": "Sobre:", - "AboutRyujinxAboutContent": "Ryujinx é um emulador de Nintendo Switch™.\nPor favor, nos dê apoio no Patreon.\nFique por dentro de todas as novidades no Twitter ou Discord.\nDesenvolvedores com interesse em contribuir podem conseguir mais informações no GitHub ou Discord.", + "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", "AboutRyujinxMaintainersTitle": "Mantido por:", + "AboutRyujinxFormerMaintainersTitle": "Formerly Maintained By:", "AboutRyujinxMaintainersContentTooltipMessage": "Clique para abrir a página de contribuidores no seu navegador padrão.", - "AboutRyujinxSupprtersTitle": "Apoiado no Patreon por:", "AmiiboSeriesLabel": "Franquia Amiibo", "AmiiboCharacterLabel": "Personagem", "AmiiboScanButtonLabel": "Escanear", diff --git a/src/Ryujinx/Assets/Locales/ru_RU.json b/src/Ryujinx/Assets/Locales/ru_RU.json index f058154e9..eb93de21d 100644 --- a/src/Ryujinx/Assets/Locales/ru_RU.json +++ b/src/Ryujinx/Assets/Locales/ru_RU.json @@ -552,15 +552,13 @@ "AboutUrlTooltipMessage": "Нажмите, чтобы открыть веб-сайт Ryujinx", "AboutDisclaimerMessage": "Ryujinx никоим образом не связан ни с Nintendo™, ни с кем-либо из ее партнеров.", "AboutAmiiboDisclaimerMessage": "Amiibo API (www.amiiboapi.com) используется для эмуляции Amiibo.", - "AboutPatreonUrlTooltipMessage": "Нажмите, чтобы открыть страницу Ryujinx на Patreon", "AboutGithubUrlTooltipMessage": "Нажмите, чтобы открыть страницу Ryujinx на GitHub", "AboutDiscordUrlTooltipMessage": "Нажмите, чтобы открыть приглашение на сервер Ryujinx в Discord", - "AboutTwitterUrlTooltipMessage": "Нажмите, чтобы открыть страницу Ryujinx в X (бывший Twitter)", "AboutRyujinxAboutTitle": "О программе:", - "AboutRyujinxAboutContent": "Ryujinx — это эмулятор Nintendo Switch™.\nПожалуйста, поддержите нас на Patreon.\nЧитайте последние новости в наших X (Twitter) или Discord.\nРазработчики, заинтересованные в участии, могут ознакомиться с проектом на GitHub или в Discord.", + "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", "AboutRyujinxMaintainersTitle": "Разработка:", + "AboutRyujinxFormerMaintainersTitle": "Formerly Maintained By:", "AboutRyujinxMaintainersContentTooltipMessage": "Нажмите, чтобы открыть страницу с участниками", - "AboutRyujinxSupprtersTitle": "Поддержка на Patreon:", "AmiiboSeriesLabel": "Серия Amiibo", "AmiiboCharacterLabel": "Персонаж", "AmiiboScanButtonLabel": "Сканировать", diff --git a/src/Ryujinx/Assets/Locales/th_TH.json b/src/Ryujinx/Assets/Locales/th_TH.json index 33b2c4f30..6364084c7 100644 --- a/src/Ryujinx/Assets/Locales/th_TH.json +++ b/src/Ryujinx/Assets/Locales/th_TH.json @@ -552,15 +552,13 @@ "AboutUrlTooltipMessage": "คลิกเพื่อเปิดเว็บไซต์ Ryujinx บนเบราว์เซอร์เริ่มต้นของคุณ", "AboutDisclaimerMessage": "ทางผู้พัฒนาโปรแกรม Ryujinx ไม่มีส่วนเกี่ยวข้องกับทางบริษัท Nintendo™\nหรือพันธมิตรใดๆ ทั้งสิ้น!", "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) ถูกใช้\nในการจำลอง อะมิโบ ของเรา", - "AboutPatreonUrlTooltipMessage": "คลิกเพื่อเปิดหน้า Patreon ของ Ryujinx บนเบราว์เซอร์เริ่มต้นของคุณ", "AboutGithubUrlTooltipMessage": "คลิกเพื่อเปิดหน้า Github ของ Ryujinx บนเบราว์เซอร์เริ่มต้นของคุณ", "AboutDiscordUrlTooltipMessage": "คลิกเพื่อเปิดคำเชิญเข้าสู่เซิร์ฟเวอร์ Discord ของ Ryujinx บนเบราว์เซอร์เริ่มต้นของคุณ", - "AboutTwitterUrlTooltipMessage": "คลิกเพื่อเปิดหน้าเพจ Twitter ของ Ryujinx บนเบราว์เซอร์เริ่มต้นของคุณ", "AboutRyujinxAboutTitle": "เกี่ยวกับ:", - "AboutRyujinxAboutContent": "Ryujinx เป็นอีมูเลเตอร์สำหรับ Nintendo Switch™\nโปรดสนับสนุนเราบน Patreon\nรับข่าวสารล่าสุดทั้งหมดบน Twitter หรือ Discord ของเรา\nนักพัฒนาที่สนใจจะมีส่วนร่วมสามารถดูข้อมูลเพิ่มเติมได้ที่ GitHub หรือ Discord ของเรา", + "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", "AboutRyujinxMaintainersTitle": "ได้รับการดูแลโดย:", + "AboutRyujinxFormerMaintainersTitle": "Formerly Maintained By:", "AboutRyujinxMaintainersContentTooltipMessage": "คลิกเพื่อเปิดหน้าผู้มีส่วนร่วมบนเบราว์เซอร์เริ่มต้นของคุณ", - "AboutRyujinxSupprtersTitle": "ผู้สนับสนุนบน Patreon:", "AmiiboSeriesLabel": "Amiibo Series", "AmiiboCharacterLabel": "ตัวละคร", "AmiiboScanButtonLabel": "สแกนเลย", diff --git a/src/Ryujinx/Assets/Locales/tr_TR.json b/src/Ryujinx/Assets/Locales/tr_TR.json index 72da205cb..1be779d67 100644 --- a/src/Ryujinx/Assets/Locales/tr_TR.json +++ b/src/Ryujinx/Assets/Locales/tr_TR.json @@ -552,15 +552,13 @@ "AboutUrlTooltipMessage": "Ryujinx'in websitesini varsayılan tarayıcınızda açmak için tıklayın.", "AboutDisclaimerMessage": "Ryujinx, Nintendo™ veya ortaklarıyla herhangi bir şekilde bağlantılı değildir.", "AboutAmiiboDisclaimerMessage": "Amiibo emülasyonumuzda \nAmiiboAPI (www.amiiboapi.com) kullanılmaktadır.", - "AboutPatreonUrlTooltipMessage": "Ryujinx'in Patreon sayfasını varsayılan tarayıcınızda açmak için tıklayın.", "AboutGithubUrlTooltipMessage": "Ryujinx'in GitHub sayfasını varsayılan tarayıcınızda açmak için tıklayın.", "AboutDiscordUrlTooltipMessage": "Varsayılan tarayıcınızda Ryujinx'in Discord'una bir davet açmak için tıklayın.", - "AboutTwitterUrlTooltipMessage": "Ryujinx'in Twitter sayfasını varsayılan tarayıcınızda açmak için tıklayın.", "AboutRyujinxAboutTitle": "Hakkında:", - "AboutRyujinxAboutContent": "Ryujinx bir Nintendo Switch™ emülatörüdür.\nLütfen bizi Patreon'da destekleyin.\nEn son haberleri Twitter veya Discord'umuzdan alın.\nKatkıda bulunmak isteyen geliştiriciler GitHub veya Discord üzerinden daha fazla bilgi edinebilir.", + "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", "AboutRyujinxMaintainersTitle": "Geliştiriciler:", + "AboutRyujinxFormerMaintainersTitle": "Formerly Maintained By:", "AboutRyujinxMaintainersContentTooltipMessage": "Katkıda bulunanlar sayfasını varsayılan tarayıcınızda açmak için tıklayın.", - "AboutRyujinxSupprtersTitle": "Patreon Destekleyicileri:", "AmiiboSeriesLabel": "Amiibo Serisi", "AmiiboCharacterLabel": "Karakter", "AmiiboScanButtonLabel": "Tarat", diff --git a/src/Ryujinx/Assets/Locales/uk_UA.json b/src/Ryujinx/Assets/Locales/uk_UA.json index 06f658640..a5d519398 100644 --- a/src/Ryujinx/Assets/Locales/uk_UA.json +++ b/src/Ryujinx/Assets/Locales/uk_UA.json @@ -552,15 +552,13 @@ "AboutUrlTooltipMessage": "Натисніть, щоб відкрити сайт Ryujinx у браузері за замовчування.", "AboutDisclaimerMessage": "Ryujinx жодним чином не пов’язано з Nintendo™,\nчи будь-яким із їхніх партнерів.", "AboutAmiiboDisclaimerMessage": "AmiiboAPI (www.amiiboapi.com) використовується в нашій емуляції Amiibo.", - "AboutPatreonUrlTooltipMessage": "Натисніть, щоб відкрити сторінку Patreon Ryujinx у вашому браузері за замовчування.", "AboutGithubUrlTooltipMessage": "Натисніть, щоб відкрити сторінку GitHub Ryujinx у браузері за замовчуванням.", "AboutDiscordUrlTooltipMessage": "Натисніть, щоб відкрити запрошення на сервер Discord Ryujinx у браузері за замовчуванням.", - "AboutTwitterUrlTooltipMessage": "Натисніть, щоб відкрити сторінку Twitter Ryujinx у браузері за замовчуванням.", "AboutRyujinxAboutTitle": "Про програму:", - "AboutRyujinxAboutContent": "Ryujinx — це емулятор для Nintendo Switch™.\nБудь ласка, підтримайте нас на Patreon.\nОтримуйте всі останні новини в нашому Twitter або Discord.\nРозробники, які хочуть зробити внесок, можуть дізнатися більше на нашому GitHub або в Discord.", + "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", "AboutRyujinxMaintainersTitle": "Підтримується:", + "AboutRyujinxFormerMaintainersTitle": "Formerly Maintained By:", "AboutRyujinxMaintainersContentTooltipMessage": "Натисніть, щоб відкрити сторінку співавторів у вашому браузері за замовчування.", - "AboutRyujinxSupprtersTitle": "Підтримується на Patreon:", "AmiiboSeriesLabel": "Серія Amiibo", "AmiiboCharacterLabel": "Персонаж", "AmiiboScanButtonLabel": "Сканувати", diff --git a/src/Ryujinx/Assets/Locales/zh_CN.json b/src/Ryujinx/Assets/Locales/zh_CN.json index 66ac309de..8b8e5d37c 100644 --- a/src/Ryujinx/Assets/Locales/zh_CN.json +++ b/src/Ryujinx/Assets/Locales/zh_CN.json @@ -552,15 +552,13 @@ "AboutUrlTooltipMessage": "在浏览器中打开 Ryujinx 模拟器官网。", "AboutDisclaimerMessage": "Ryujinx 与 Nintendo™ 以及其合作伙伴没有任何关联。", "AboutAmiiboDisclaimerMessage": "我们的 Amiibo 模拟使用了\nAmiiboAPI (www.amiiboapi.com)。", - "AboutPatreonUrlTooltipMessage": "在浏览器中打开 Ryujinx 的 Patreon 赞助页。", "AboutGithubUrlTooltipMessage": "在浏览器中打开 Ryujinx 的 GitHub 代码库。", "AboutDiscordUrlTooltipMessage": "在浏览器中打开 Ryujinx 的 Discord 邀请链接。", - "AboutTwitterUrlTooltipMessage": "在浏览器中打开 Ryujinx 的 Twitter 主页。", "AboutRyujinxAboutTitle": "关于:", - "AboutRyujinxAboutContent": "Ryujinx 是一款 Nintendo Switch™ 模拟器。\n您可以在 Patreon 上赞助 Ryujinx。\n关注 Twitter 或 Discord 可以获取模拟器最新动态。\n如果您对开发感兴趣,欢迎来 GitHub 或 Discord 加入我们!", + "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", "AboutRyujinxMaintainersTitle": "开发维护人员名单:", + "AboutRyujinxFormerMaintainersTitle": "Formerly Maintained By:", "AboutRyujinxMaintainersContentTooltipMessage": "在浏览器中打开贡献者页面", - "AboutRyujinxSupprtersTitle": "感谢 Patreon 上的赞助者:", "AmiiboSeriesLabel": "Amiibo 系列", "AmiiboCharacterLabel": "角色", "AmiiboScanButtonLabel": "扫描", diff --git a/src/Ryujinx/Assets/Locales/zh_TW.json b/src/Ryujinx/Assets/Locales/zh_TW.json index 64f137885..46761ff02 100644 --- a/src/Ryujinx/Assets/Locales/zh_TW.json +++ b/src/Ryujinx/Assets/Locales/zh_TW.json @@ -552,15 +552,13 @@ "AboutUrlTooltipMessage": "在預設瀏覽器中開啟 Ryujinx 網站。", "AboutDisclaimerMessage": "Ryujinx 和 Nintendo™\n或其任何合作夥伴完全沒有關聯。", "AboutAmiiboDisclaimerMessage": "我們在 Amiibo 模擬中\n使用了 AmiiboAPI (www.amiiboapi.com)。", - "AboutPatreonUrlTooltipMessage": "在預設瀏覽器中開啟 Ryujinx 的 Patreon 網頁。", "AboutGithubUrlTooltipMessage": "在預設瀏覽器中開啟 Ryujinx 的 GitHub 網頁。", "AboutDiscordUrlTooltipMessage": "在預設瀏覽器中開啟 Ryujinx 的 Discord 邀請連結。", - "AboutTwitterUrlTooltipMessage": "在預設瀏覽器中開啟 Ryujinx 的 Twitter 網頁。", "AboutRyujinxAboutTitle": "關於:", - "AboutRyujinxAboutContent": "Ryujinx 是一款 Nintendo Switch™ 模擬器。\n請在 Patreon 上支持我們。\n關注我們的 Twitter 或 Discord 取得所有最新消息。\n對於有興趣貢獻的開發者,可以在我們的 GitHub 或 Discord 上了解更多資訊。", + "AboutRyujinxAboutContent": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", "AboutRyujinxMaintainersTitle": "維護者:", + "AboutRyujinxFormerMaintainersTitle": "Formerly Maintained By:", "AboutRyujinxMaintainersContentTooltipMessage": "在預設瀏覽器中開啟貢獻者的網頁", - "AboutRyujinxSupprtersTitle": "Patreon 支持者:", "AmiiboSeriesLabel": "Amiibo 系列", "AmiiboCharacterLabel": "角色", "AmiiboScanButtonLabel": "掃描", From 39252b7267a38288f18449356ba20d793883fe04 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 8 Dec 2024 13:04:01 -0600 Subject: [PATCH 18/79] UI: Update About window with the current status of the project. --- src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs | 4 +++- src/Ryujinx/UI/Windows/AboutWindow.axaml | 11 ++++++++++- src/Ryujinx/UI/Windows/AboutWindow.axaml.cs | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs index c48ad378f..23d0f963c 100644 --- a/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs @@ -45,7 +45,9 @@ namespace Ryujinx.Ava.UI.ViewModels } } - public string Developers => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.AboutPageDeveloperListMore, "gdkchan, Ac_K, marysaka, rip in peri peri, LDj3SNuD, emmaus, Thealexbarney, GoffyDude, TSRBerry, IsaacMarovitz, GreemDev"); + public string Developers => "GreemDev"; + + public string FormerDevelopers => LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.AboutPageDeveloperListMore, "gdkchan, Ac_K, marysaka, rip in peri peri, LDj3SNuD, emmaus, Thealexbarney, GoffyDude, TSRBerry, IsaacMarovitz"); public AboutWindowViewModel() { diff --git a/src/Ryujinx/UI/Windows/AboutWindow.axaml b/src/Ryujinx/UI/Windows/AboutWindow.axaml index 1d0e36ae9..bce0fde57 100644 --- a/src/Ryujinx/UI/Windows/AboutWindow.axaml +++ b/src/Ryujinx/UI/Windows/AboutWindow.axaml @@ -165,7 +165,16 @@ Text="{ext:Locale AboutRyujinxMaintainersTitle}" /> + + + + + ViewModel.MatchSystemTime(); } } From 0f18df982f44d8edec9b9f55400c6ca95b691fae Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 21 Dec 2024 19:59:16 -0600 Subject: [PATCH 49/79] UI: localize the button & make it smaller --- src/Ryujinx/Assets/locales.json | 48 +++++++++++++++++++ .../UI/ViewModels/SettingsViewModel.cs | 3 -- .../Views/Settings/SettingsSystemView.axaml | 8 ++-- 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 9fa113593..1b4624c95 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -3765,6 +3765,30 @@ "zh_TW": "系統時鐘:" } }, + { + "ID": "SettingsTabSystemSystemTimeMatch", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Match PC Time", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "SettingsTabSystemEnablePptc", "Translations": { @@ -14541,6 +14565,30 @@ "zh_TW": "變更系統時鐘" } }, + { + "ID": "MatchTimeTooltip", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "Change System Time to match your PC's time.", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "VSyncToggleTooltip", "Translations": { diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 8edf71a39..0824e3f86 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -329,9 +329,6 @@ namespace Ryujinx.Ava.UI.ViewModels } } - //private DateTimeOffset _currentDate; - //private TimeSpan _currentTime; - public DateTimeOffset CurrentDate { get; set; } public TimeSpan CurrentTime { get; set; } diff --git a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml index abb1a2e49..73cc70a23 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml @@ -183,15 +183,15 @@ ToolTip.Tip="{ext:Locale TimeTooltip}" /> From 2fac0f4db11b934320dfc26edb984ca5972ca3dc Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 21 Dec 2024 20:00:16 -0600 Subject: [PATCH 50/79] Specify it's date & time --- src/Ryujinx/Assets/locales.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 1b4624c95..4c426f062 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -14571,7 +14571,7 @@ "ar_SA": "", "de_DE": "", "el_GR": "", - "en_US": "Change System Time to match your PC's time.", + "en_US": "Change System Time to match your PC's date & time.", "es_ES": "", "fr_FR": "", "he_IL": "", From f898a5ecf4370c1d286e6187c8f00981f44244de Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 21 Dec 2024 20:06:59 -0600 Subject: [PATCH 51/79] Remove code references to having a flatpak version --- src/Ryujinx.Common/ReleaseInformation.cs | 3 --- .../Helper/FileAssociationHelper.cs | 2 +- src/Ryujinx/Assets/locales.json | 24 ------------------- .../UI/Controls/ApplicationContextMenu.axaml | 1 - .../UI/ViewModels/MainWindowViewModel.cs | 2 -- src/Ryujinx/Updater.cs | 19 ++++----------- 6 files changed, 5 insertions(+), 46 deletions(-) diff --git a/src/Ryujinx.Common/ReleaseInformation.cs b/src/Ryujinx.Common/ReleaseInformation.cs index 011d9848a..cbf93013f 100644 --- a/src/Ryujinx.Common/ReleaseInformation.cs +++ b/src/Ryujinx.Common/ReleaseInformation.cs @@ -6,7 +6,6 @@ namespace Ryujinx.Common // DO NOT EDIT, filled by CI public static class ReleaseInformation { - private const string FlatHubChannel = "flathub"; private const string CanaryChannel = "canary"; private const string ReleaseChannel = "release"; @@ -29,8 +28,6 @@ namespace Ryujinx.Common !ReleaseChannelRepo.StartsWith("%%") && !ConfigFileName.StartsWith("%%"); - public static bool IsFlatHubBuild => IsValid && ReleaseChannelOwner.Equals(FlatHubChannel); - public static bool IsCanaryBuild => IsValid && ReleaseChannelName.Equals(CanaryChannel); public static bool IsReleaseBuild => IsValid && ReleaseChannelName.Equals(ReleaseChannel); diff --git a/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs b/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs index 9333a1b76..44860d080 100644 --- a/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs +++ b/src/Ryujinx.UI.Common/Helper/FileAssociationHelper.cs @@ -23,7 +23,7 @@ namespace Ryujinx.UI.Common.Helper [LibraryImport("shell32.dll", SetLastError = true)] public static partial void SHChangeNotify(uint wEventId, uint uFlags, nint dwItem1, nint dwItem2); - public static bool IsTypeAssociationSupported => (OperatingSystem.IsLinux() || OperatingSystem.IsWindows()) && !ReleaseInformation.IsFlatHubBuild; + public static bool IsTypeAssociationSupported => (OperatingSystem.IsLinux() || OperatingSystem.IsWindows()); public static bool AreMimeTypesRegistered { diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 4c426f062..007f1d1e6 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -16173,30 +16173,6 @@ "zh_TW": "CPU 模式" } }, - { - "ID": "DialogUpdaterFlatpakNotSupportedMessage", - "Translations": { - "ar_SA": "الرجاء تحديث ريوجينكس عبر فلات هاب.", - "de_DE": "Bitte aktualisiere Ryujinx über FlatHub", - "el_GR": "Παρακαλούμε ενημερώστε το Ryujinx μέσω FlatHub.", - "en_US": "Please update Ryujinx via FlatHub.", - "es_ES": "Por favor, actualiza Ryujinx a través de FlatHub.", - "fr_FR": "Merci de mettre à jour Ryujinx via FlatHub.", - "he_IL": "בבקשה עדכן את ריוג'ינקס דרך פלאטהב.", - "it_IT": "Aggiorna Ryujinx tramite FlatHub.", - "ja_JP": "FlatHub を使用して Ryujinx をアップデートしてください.", - "ko_KR": "FlatHub를 통해 Ryujinx를 업데이트하세요.", - "no_NO": "Vennligst oppdater Ryujinx via FlatHub.", - "pl_PL": "Zaktualizuj Ryujinx przez FlatHub.", - "pt_BR": "Por favor, atualize o Ryujinx pelo FlatHub.", - "ru_RU": "Пожалуйста, обновите Ryujinx через FlatHub.", - "th_TH": "โปรดอัปเดต Ryujinx ผ่านช่องทาง FlatHub", - "tr_TR": "Lütfen Ryujinx'i FlatHub aracılığıyla güncelleyin.", - "uk_UA": "", - "zh_CN": "请通过 FlatHub 更新 Ryujinx 模拟器。", - "zh_TW": "請透過 Flathub 更新 Ryujinx。" - } - }, { "ID": "UpdaterDisabledWarningTitle", "Translations": { diff --git a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml index 951f7f616..7708936ca 100644 --- a/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationContextMenu.axaml @@ -17,7 +17,6 @@ diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index cebd65701..ae373c267 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -424,8 +424,6 @@ namespace Ryujinx.Ava.UI.ViewModels public bool OpenBcatSaveDirectoryEnabled => !SelectedApplication.ControlHolder.ByteSpan.IsZeros() && SelectedApplication.ControlHolder.Value.BcatDeliveryCacheStorageSize > 0; - public bool CreateShortcutEnabled => !ReleaseInformation.IsFlatHubBuild; - public string LoadHeading { get => _loadHeading; diff --git a/src/Ryujinx/Updater.cs b/src/Ryujinx/Updater.cs index e240ad141..21d991d97 100644 --- a/src/Ryujinx/Updater.cs +++ b/src/Ryujinx/Updater.cs @@ -686,22 +686,11 @@ namespace Ryujinx.Ava #else if (showWarnings) { - if (ReleaseInformation.IsFlatHubBuild) - { - Dispatcher.UIThread.InvokeAsync(() => - ContentDialogHelper.CreateWarningDialog( - LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle], - LocaleManager.Instance[LocaleKeys.DialogUpdaterFlatpakNotSupportedMessage]) + Dispatcher.UIThread.InvokeAsync(() => + ContentDialogHelper.CreateWarningDialog( + LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle], + LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage]) ); - } - else - { - Dispatcher.UIThread.InvokeAsync(() => - ContentDialogHelper.CreateWarningDialog( - LocaleManager.Instance[LocaleKeys.UpdaterDisabledWarningTitle], - LocaleManager.Instance[LocaleKeys.DialogUpdaterDirtyBuildSubMessage]) - ); - } } return false; From 4c7cb54ec63f907523a9c56b4cbcf899a6b893ef Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sat, 21 Dec 2024 21:52:04 -0600 Subject: [PATCH 52/79] misc: I may be stupid --- src/Ryujinx.HLE/HOS/Kernel/Common/KResourceLimit.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Ryujinx.HLE/HOS/Kernel/Common/KResourceLimit.cs b/src/Ryujinx.HLE/HOS/Kernel/Common/KResourceLimit.cs index ce2b7185a..1763edce2 100644 --- a/src/Ryujinx.HLE/HOS/Kernel/Common/KResourceLimit.cs +++ b/src/Ryujinx.HLE/HOS/Kernel/Common/KResourceLimit.cs @@ -15,7 +15,8 @@ namespace Ryujinx.HLE.HOS.Kernel.Common private readonly long[] _current2; private readonly long[] _peak; - private readonly Lock _lock = new(); + // type is not Lock due to Monitor class usage + private readonly object _lock = new(); private readonly LinkedList _waitingThreads; From 67ec10feea0db14a9647db4deebb11523c6f05da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hack=E8=8C=B6=E3=82=93?= <120134269+Hackjjang@users.noreply.github.com> Date: Sun, 22 Dec 2024 13:46:57 +0900 Subject: [PATCH 53/79] Korean translation update (#422) --- src/Ryujinx/Assets/locales.json | 54 ++++++++++++++++----------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 007f1d1e6..5df389381 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -705,7 +705,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "Amiibo 스캔(빈에서)", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -1137,7 +1137,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "자주 묻는 질문(FAQ) 및 문제해결 페이지", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -1161,7 +1161,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "공식 Ryujinx 위키에서 자주 묻는 질문(FAQ) 및 문제 해결 페이지 열기", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -1185,7 +1185,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "설치 및 구성 안내", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -1209,7 +1209,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "공식 Ryujinx 위키에서 설정 및 구성 안내 열기", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -1233,7 +1233,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "멀티플레이어(LDN/LAN) 안내", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -1257,7 +1257,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "공식 Ryujinx 위키에서 멀티플레이어 안내 열기", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -8073,7 +8073,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "지우기", + "ko_KR": "", "no_NO": "Tøm", "pl_PL": "", "pt_BR": "", @@ -11865,7 +11865,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "{0} : {1}", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -18777,7 +18777,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "{0:n0}MB", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -19065,7 +19065,7 @@ "he_IL": "{0} הרחבות משחק", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "{0} DLC 사용 가능", "no_NO": "{0} Nedlastbare innhold(er)", "pl_PL": "", "pt_BR": "", @@ -21201,7 +21201,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "수직 동기화 :", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -21225,7 +21225,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "사용자 정의 주사율 활성화(실험적)", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -21249,7 +21249,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "스위치", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -21273,7 +21273,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "무제한", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -21297,7 +21297,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "사용자 정의 주사율", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -21321,7 +21321,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "에뮬레이트된 수직 동기화. '스위치'는 스위치의 60Hz 주사율을 에뮬레이트합니다. '무한'은 무제한 주사율입니다.", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -21345,7 +21345,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "에뮬레이트된 수직 동기화. '스위치'는 스위치의 60Hz 주사율을 에뮬레이트합니다. '무한'은 무제한 주사율입니다. '사용자 지정'은 지정된 사용자 지정 주사율을 에뮬레이트합니다.", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -21369,7 +21369,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "사용자가 에뮬레이트된 화면 주사율을 지정할 수 있습니다. 일부 타이틀에서는 게임플레이 로직 속도가 빨라지거나 느려질 수 있습니다. 다른 타이틀에서는 주사율의 배수로 FPS를 제한하거나 예측할 수 없는 동작으로 이어질 수 있습니다. 이는 실험적 기능으로 게임 플레이에 어떤 영향을 미칠지 보장할 수 없습니다. \n\n모르면 끔으로 두세요.", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -21393,7 +21393,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "사용자 정의 주사율 목표 값입니다.", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -21417,7 +21417,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "일반 스위치 주사율의 백분율로 나타낸 사용자 지정 주사율입니다.", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -21441,7 +21441,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "사용자 정의 주사율 % :", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -21465,7 +21465,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "사용자 정의 주사율 값 :", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -21489,7 +21489,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "간격", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -21513,7 +21513,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "수직 동기화 모드 전환 :", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -21537,7 +21537,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "사용자 정의 주사율 증가", "no_NO": "", "pl_PL": "", "pt_BR": "", @@ -21561,7 +21561,7 @@ "he_IL": "", "it_IT": "", "ja_JP": "", - "ko_KR": "", + "ko_KR": "사용자 정의 주사율 감소", "no_NO": "", "pl_PL": "", "pt_BR": "", From decd37ce6d670d6a1136409292d253dabd69475d Mon Sep 17 00:00:00 2001 From: Marco Carvalho Date: Sun, 22 Dec 2024 02:28:31 -0300 Subject: [PATCH 54/79] Add missing "yield return" (#424) --- src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs | 34 +++++++-------- src/Ryujinx.Cpu/Jit/MemoryManager.cs | 34 +++++++-------- .../Jit/MemoryManagerHostTracked.cs | 14 +++---- .../LightningJit/CodeGen/Arm64/StackWalker.cs | 6 +-- .../BufferMirrorRangeList.cs | 10 ++--- src/Ryujinx.HLE/HOS/ModLoader.cs | 9 ++-- src/Ryujinx.Memory/AddressSpaceManager.cs | 42 +++++++------------ 7 files changed, 54 insertions(+), 95 deletions(-) diff --git a/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs b/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs index bb56a4344..74c39d6a8 100644 --- a/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs +++ b/src/Ryujinx.Cpu/AppleHv/HvMemoryManager.cs @@ -230,25 +230,20 @@ namespace Ryujinx.Cpu.AppleHv { if (size == 0) { - return Enumerable.Empty(); + yield break; } var guestRegions = GetPhysicalRegionsImpl(va, size); if (guestRegions == null) { - return null; + yield break; } - var regions = new HostMemoryRange[guestRegions.Count]; - - for (int i = 0; i < regions.Length; i++) + foreach (var guestRegion in guestRegions) { - var guestRegion = guestRegions[i]; nint pointer = _backingMemory.GetPointer(guestRegion.Address, guestRegion.Size); - regions[i] = new HostMemoryRange((nuint)(ulong)pointer, guestRegion.Size); + yield return new HostMemoryRange((nuint)(ulong)pointer, guestRegion.Size); } - - return regions; } /// @@ -256,23 +251,24 @@ namespace Ryujinx.Cpu.AppleHv { if (size == 0) { - return Enumerable.Empty(); + yield break; } - return GetPhysicalRegionsImpl(va, size); + foreach (var physicalRegion in GetPhysicalRegionsImpl(va, size)) + { + yield return physicalRegion; + } } - private List GetPhysicalRegionsImpl(ulong va, ulong size) + private IEnumerable GetPhysicalRegionsImpl(ulong va, ulong size) { if (!ValidateAddress(va) || !ValidateAddressAndSize(va, size)) { - return null; + yield break; } int pages = GetPagesCount(va, (uint)size, out va); - var regions = new List(); - ulong regionStart = GetPhysicalAddressInternal(va); ulong regionSize = PageSize; @@ -280,14 +276,14 @@ namespace Ryujinx.Cpu.AppleHv { if (!ValidateAddress(va + PageSize)) { - return null; + yield break; } ulong newPa = GetPhysicalAddressInternal(va + PageSize); if (GetPhysicalAddressInternal(va) + PageSize != newPa) { - regions.Add(new MemoryRange(regionStart, regionSize)); + yield return new MemoryRange(regionStart, regionSize); regionStart = newPa; regionSize = 0; } @@ -296,9 +292,7 @@ namespace Ryujinx.Cpu.AppleHv regionSize += PageSize; } - regions.Add(new MemoryRange(regionStart, regionSize)); - - return regions; + yield return new MemoryRange(regionStart, regionSize); } /// diff --git a/src/Ryujinx.Cpu/Jit/MemoryManager.cs b/src/Ryujinx.Cpu/Jit/MemoryManager.cs index 049e508d0..076fb6ad8 100644 --- a/src/Ryujinx.Cpu/Jit/MemoryManager.cs +++ b/src/Ryujinx.Cpu/Jit/MemoryManager.cs @@ -250,25 +250,20 @@ namespace Ryujinx.Cpu.Jit { if (size == 0) { - return Enumerable.Empty(); + yield break; } var guestRegions = GetPhysicalRegionsImpl(va, size); if (guestRegions == null) { - return null; + yield break; } - var regions = new HostMemoryRange[guestRegions.Count]; - - for (int i = 0; i < regions.Length; i++) + foreach (var guestRegion in guestRegions) { - var guestRegion = guestRegions[i]; nint pointer = _backingMemory.GetPointer(guestRegion.Address, guestRegion.Size); - regions[i] = new HostMemoryRange((nuint)(ulong)pointer, guestRegion.Size); + yield return new HostMemoryRange((nuint)(ulong)pointer, guestRegion.Size); } - - return regions; } /// @@ -276,23 +271,24 @@ namespace Ryujinx.Cpu.Jit { if (size == 0) { - return Enumerable.Empty(); + yield break; } - return GetPhysicalRegionsImpl(va, size); + foreach (var physicalRegion in GetPhysicalRegionsImpl(va, size)) + { + yield return physicalRegion; + } } - private List GetPhysicalRegionsImpl(ulong va, ulong size) + private IEnumerable GetPhysicalRegionsImpl(ulong va, ulong size) { if (!ValidateAddress(va) || !ValidateAddressAndSize(va, size)) { - return null; + yield break; } int pages = GetPagesCount(va, (uint)size, out va); - var regions = new List(); - ulong regionStart = GetPhysicalAddressInternal(va); ulong regionSize = PageSize; @@ -300,14 +296,14 @@ namespace Ryujinx.Cpu.Jit { if (!ValidateAddress(va + PageSize)) { - return null; + yield break; } ulong newPa = GetPhysicalAddressInternal(va + PageSize); if (GetPhysicalAddressInternal(va) + PageSize != newPa) { - regions.Add(new MemoryRange(regionStart, regionSize)); + yield return new MemoryRange(regionStart, regionSize); regionStart = newPa; regionSize = 0; } @@ -316,9 +312,7 @@ namespace Ryujinx.Cpu.Jit regionSize += PageSize; } - regions.Add(new MemoryRange(regionStart, regionSize)); - - return regions; + yield return new MemoryRange(regionStart, regionSize); } /// diff --git a/src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs b/src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs index 4dab212a7..499f991f2 100644 --- a/src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs +++ b/src/Ryujinx.Cpu/Jit/MemoryManagerHostTracked.cs @@ -475,17 +475,15 @@ namespace Ryujinx.Cpu.Jit return GetPhysicalRegionsImpl(va, size); } - private List GetPhysicalRegionsImpl(ulong va, ulong size) + private IEnumerable GetPhysicalRegionsImpl(ulong va, ulong size) { if (!ValidateAddress(va) || !ValidateAddressAndSize(va, size)) { - return null; + yield break; } int pages = GetPagesCount(va, (uint)size, out va); - var regions = new List(); - ulong regionStart = GetPhysicalAddressInternal(va); ulong regionSize = PageSize; @@ -493,14 +491,14 @@ namespace Ryujinx.Cpu.Jit { if (!ValidateAddress(va + PageSize)) { - return null; + yield break; } ulong newPa = GetPhysicalAddressInternal(va + PageSize); if (GetPhysicalAddressInternal(va) + PageSize != newPa) { - regions.Add(new MemoryRange(regionStart, regionSize)); + yield return new MemoryRange(regionStart, regionSize); regionStart = newPa; regionSize = 0; } @@ -509,9 +507,7 @@ namespace Ryujinx.Cpu.Jit regionSize += PageSize; } - regions.Add(new MemoryRange(regionStart, regionSize)); - - return regions; + yield return new MemoryRange(regionStart, regionSize); } /// diff --git a/src/Ryujinx.Cpu/LightningJit/CodeGen/Arm64/StackWalker.cs b/src/Ryujinx.Cpu/LightningJit/CodeGen/Arm64/StackWalker.cs index 3ce7c4f9c..1432c4598 100644 --- a/src/Ryujinx.Cpu/LightningJit/CodeGen/Arm64/StackWalker.cs +++ b/src/Ryujinx.Cpu/LightningJit/CodeGen/Arm64/StackWalker.cs @@ -8,8 +8,6 @@ namespace Ryujinx.Cpu.LightningJit.CodeGen.Arm64 { public IEnumerable GetCallStack(nint framePointer, nint codeRegionStart, int codeRegionSize, nint codeRegion2Start, int codeRegion2Size) { - List functionPointers = new(); - while (true) { nint functionPointer = Marshal.ReadIntPtr(framePointer, nint.Size); @@ -20,11 +18,9 @@ namespace Ryujinx.Cpu.LightningJit.CodeGen.Arm64 break; } - functionPointers.Add((ulong)functionPointer - 4); + yield return (ulong)functionPointer - 4; framePointer = Marshal.ReadIntPtr(framePointer); } - - return functionPointers; } } } diff --git a/src/Ryujinx.Graphics.Vulkan/BufferMirrorRangeList.cs b/src/Ryujinx.Graphics.Vulkan/BufferMirrorRangeList.cs index 5722ca1ac..e79248a47 100644 --- a/src/Ryujinx.Graphics.Vulkan/BufferMirrorRangeList.cs +++ b/src/Ryujinx.Graphics.Vulkan/BufferMirrorRangeList.cs @@ -168,16 +168,14 @@ namespace Ryujinx.Graphics.Vulkan return BinarySearch(list, offset, size) >= 0; } - public readonly List FindOverlaps(int offset, int size) + public readonly IEnumerable FindOverlaps(int offset, int size) { var list = _ranges; if (list == null) { - return null; + yield break; } - List result = null; - int index = BinarySearch(list, offset, size); if (index >= 0) @@ -189,12 +187,10 @@ namespace Ryujinx.Graphics.Vulkan do { - (result ??= new List()).Add(list[index++]); + yield return list[index++]; } while (index < list.Count && list[index].OverlapsWith(offset, size)); } - - return result; } private static int BinarySearch(List list, int offset, int size) diff --git a/src/Ryujinx.HLE/HOS/ModLoader.cs b/src/Ryujinx.HLE/HOS/ModLoader.cs index d8c62fc66..4bd695ae5 100644 --- a/src/Ryujinx.HLE/HOS/ModLoader.cs +++ b/src/Ryujinx.HLE/HOS/ModLoader.cs @@ -357,7 +357,6 @@ namespace Ryujinx.HLE.HOS { string cheatName = DefaultCheatName; List instructions = new(); - List cheats = new(); using StreamReader cheatData = cheatFile.OpenText(); while (cheatData.ReadLine() is { } line) @@ -373,13 +372,13 @@ namespace Ryujinx.HLE.HOS Logger.Warning?.Print(LogClass.ModLoader, $"Ignoring cheat '{cheatFile.FullName}' because it is malformed"); - return Array.Empty(); + yield break; } // Add the previous section to the list. if (instructions.Count > 0) { - cheats.Add(new Cheat($"<{cheatName} Cheat>", cheatFile, instructions)); + yield return new Cheat($"<{cheatName} Cheat>", cheatFile, instructions); } // Start a new cheat section. @@ -396,10 +395,8 @@ namespace Ryujinx.HLE.HOS // Add the last section being processed. if (instructions.Count > 0) { - cheats.Add(new Cheat($"<{cheatName} Cheat>", cheatFile, instructions)); + yield return new Cheat($"<{cheatName} Cheat>", cheatFile, instructions); } - - return cheats; } // Assumes searchDirPaths don't overlap diff --git a/src/Ryujinx.Memory/AddressSpaceManager.cs b/src/Ryujinx.Memory/AddressSpaceManager.cs index 807c5c0f4..7bd572d7a 100644 --- a/src/Ryujinx.Memory/AddressSpaceManager.cs +++ b/src/Ryujinx.Memory/AddressSpaceManager.cs @@ -106,10 +106,13 @@ namespace Ryujinx.Memory { if (size == 0) { - return Enumerable.Empty(); + yield break; } - return GetHostRegionsImpl(va, size); + foreach (var hostRegion in GetHostRegionsImpl(va, size)) + { + yield return hostRegion; + } } /// @@ -117,51 +120,36 @@ namespace Ryujinx.Memory { if (size == 0) { - return Enumerable.Empty(); + yield break; } var hostRegions = GetHostRegionsImpl(va, size); if (hostRegions == null) { - return null; + yield break; } - var regions = new MemoryRange[hostRegions.Count]; - ulong backingStart = (ulong)_backingMemory.Pointer; ulong backingEnd = backingStart + _backingMemory.Size; - int count = 0; - - for (int i = 0; i < regions.Length; i++) + foreach (var hostRegion in hostRegions) { - var hostRegion = hostRegions[i]; - if (hostRegion.Address >= backingStart && hostRegion.Address < backingEnd) { - regions[count++] = new MemoryRange(hostRegion.Address - backingStart, hostRegion.Size); + yield return new MemoryRange(hostRegion.Address - backingStart, hostRegion.Size); } } - - if (count != regions.Length) - { - return new ArraySegment(regions, 0, count); - } - - return regions; } - private List GetHostRegionsImpl(ulong va, ulong size) + private IEnumerable GetHostRegionsImpl(ulong va, ulong size) { if (!ValidateAddress(va) || !ValidateAddressAndSize(va, size)) { - return null; + yield break; } int pages = GetPagesCount(va, size, out va); - var regions = new List(); - nuint regionStart = GetHostAddress(va); ulong regionSize = PageSize; @@ -169,14 +157,14 @@ namespace Ryujinx.Memory { if (!ValidateAddress(va + PageSize)) { - return null; + yield break; } nuint newHostAddress = GetHostAddress(va + PageSize); if (GetHostAddress(va) + PageSize != newHostAddress) { - regions.Add(new HostMemoryRange(regionStart, regionSize)); + yield return new HostMemoryRange(regionStart, regionSize); regionStart = newHostAddress; regionSize = 0; } @@ -185,9 +173,7 @@ namespace Ryujinx.Memory regionSize += PageSize; } - regions.Add(new HostMemoryRange(regionStart, regionSize)); - - return regions; + yield return new HostMemoryRange(regionStart, regionSize); } [MethodImpl(MethodImplOptions.AggressiveInlining)] From 5913ceda4061028327521ed14b1c521ce6e87cf2 Mon Sep 17 00:00:00 2001 From: Marco Carvalho Date: Sun, 22 Dec 2024 14:36:05 -0300 Subject: [PATCH 55/79] Avoid zero-length array allocations (#427) --- src/Ryujinx.HLE/HOS/Horizon.cs | 4 ++-- src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Ryujinx.HLE/HOS/Horizon.cs b/src/Ryujinx.HLE/HOS/Horizon.cs index c585aed54..f9c5ddecf 100644 --- a/src/Ryujinx.HLE/HOS/Horizon.cs +++ b/src/Ryujinx.HLE/HOS/Horizon.cs @@ -341,7 +341,7 @@ namespace Ryujinx.HLE.HOS { if (VirtualAmiibo.ApplicationBytes.Length > 0) { - VirtualAmiibo.ApplicationBytes = new byte[0]; + VirtualAmiibo.ApplicationBytes = Array.Empty(); VirtualAmiibo.InputBin = string.Empty; } if (NfpDevices[nfpDeviceId].State == NfpDeviceState.SearchingForTag) @@ -356,7 +356,7 @@ namespace Ryujinx.HLE.HOS VirtualAmiibo.InputBin = path; if (VirtualAmiibo.ApplicationBytes.Length > 0) { - VirtualAmiibo.ApplicationBytes = new byte[0]; + VirtualAmiibo.ApplicationBytes = Array.Empty(); } byte[] encryptedData = File.ReadAllBytes(path); VirtualAmiiboFile newFile = AmiiboBinReader.ReadBinFile(encryptedData); diff --git a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs index ee43fc307..2cb35472f 100644 --- a/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs +++ b/src/Ryujinx.HLE/HOS/Services/Nfc/Nfp/VirtualAmiibo.cs @@ -16,7 +16,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp static class VirtualAmiibo { public static uint OpenedApplicationAreaId; - public static byte[] ApplicationBytes = new byte[0]; + public static byte[] ApplicationBytes = Array.Empty(); public static string InputBin = string.Empty; public static string NickName = string.Empty; private static readonly AmiiboJsonSerializerContext _serializerContext = AmiiboJsonSerializerContext.Default; @@ -137,7 +137,7 @@ namespace Ryujinx.HLE.HOS.Services.Nfc.Nfp if (ApplicationBytes.Length > 0) { byte[] bytes = ApplicationBytes; - ApplicationBytes = new byte[0]; + ApplicationBytes = Array.Empty(); return bytes; } VirtualAmiiboFile virtualAmiiboFile = LoadAmiiboFile(amiiboId); From 1ea345faa7a908125ecdb278873451b5920cfed1 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 22 Dec 2024 12:53:48 -0600 Subject: [PATCH 56/79] UI: Move Match PC Time to next to the time selector & change label & tooltip to clarify behavior further. --- src/Ryujinx/Assets/locales.json | 4 ++-- src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml | 6 +----- src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml.cs | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 5df389381..167accd34 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -3771,7 +3771,7 @@ "ar_SA": "", "de_DE": "", "el_GR": "", - "en_US": "Match PC Time", + "en_US": "Resync to PC Date & Time", "es_ES": "", "fr_FR": "", "he_IL": "", @@ -14571,7 +14571,7 @@ "ar_SA": "", "de_DE": "", "el_GR": "", - "en_US": "Change System Time to match your PC's date & time.", + "en_US": "Resync System Time to match your PC's current date & time.\n\nThis is not an active setting, it can still fall out of sync; in which case just click this button again.", "es_ES": "", "fr_FR": "", "he_IL": "", diff --git a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml index 73cc70a23..9295413ba 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml @@ -181,15 +181,11 @@ SelectedTime="{Binding CurrentTime}" Width="350" ToolTip.Tip="{ext:Locale TimeTooltip}" /> - - diff --git a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml.cs b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml.cs index 5cecd7221..5103ce383 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml.cs +++ b/src/Ryujinx/UI/Views/Settings/SettingsSystemView.axaml.cs @@ -1,7 +1,7 @@ +using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; using Ryujinx.Ava.UI.ViewModels; -using System; using TimeZone = Ryujinx.Ava.UI.Models.TimeZone; namespace Ryujinx.Ava.UI.Views.Settings From 8259f790d75ca5c18ef8e3d56824b8f86dde1eff Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 22 Dec 2024 13:19:10 -0600 Subject: [PATCH 57/79] misc: Cleanup locale validator --- .../LocaleValidationTask.cs | 21 ++++++++----------- src/Ryujinx/Assets/locales.json | 2 +- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/Ryujinx.BuildValidationTasks/LocaleValidationTask.cs b/src/Ryujinx.BuildValidationTasks/LocaleValidationTask.cs index 183228fdd..6dc3d8aa8 100644 --- a/src/Ryujinx.BuildValidationTasks/LocaleValidationTask.cs +++ b/src/Ryujinx.BuildValidationTasks/LocaleValidationTask.cs @@ -14,20 +14,20 @@ namespace Ryujinx.BuildValidationTasks { string path = System.Reflection.Assembly.GetExecutingAssembly().Location; - if (path.Split(new string[] { "src" }, StringSplitOptions.None).Length == 1 ) + if (path.Split(["src"], StringSplitOptions.None).Length == 1) { //i assume that we are in a build directory in the solution dir - path = new FileInfo(path).Directory.Parent.GetDirectories("src")[0].GetDirectories("Ryujinx")[0].GetDirectories("Assets")[0].GetFiles("locales.json")[0].FullName; + path = new FileInfo(path).Directory!.Parent!.GetDirectories("src")[0].GetDirectories("Ryujinx")[0].GetDirectories("Assets")[0].GetFiles("locales.json")[0].FullName; } else { - path = path.Split(new string[] { "src" }, StringSplitOptions.None)[0]; - path = new FileInfo(path).Directory.GetDirectories("src")[0].GetDirectories("Ryujinx")[0].GetDirectories("Assets")[0].GetFiles("locales.json")[0].FullName; + path = path.Split(["src"], StringSplitOptions.None)[0]; + path = new FileInfo(path).Directory!.GetDirectories("src")[0].GetDirectories("Ryujinx")[0].GetDirectories("Assets")[0].GetFiles("locales.json")[0].FullName; } string data; - using (StreamReader sr = new StreamReader(path)) + using (StreamReader sr = new(path)) { data = sr.ReadToEnd(); } @@ -38,13 +38,10 @@ namespace Ryujinx.BuildValidationTasks { LocalesEntry locale = json.Locales[i]; - foreach (string language in json.Languages) + foreach (string langCode in json.Languages.Where(it => !locale.Translations.ContainsKey(it))) { - if (!locale.Translations.ContainsKey(language)) - { - locale.Translations.Add(language, ""); - Log.LogMessage(MessageImportance.High, $"Added {{{language}}} to Locale {{{locale.ID}}}"); - } + locale.Translations.Add(langCode, string.Empty); + Log.LogMessage(MessageImportance.High, $"Added '{langCode}' to Locale '{locale.ID}'"); } locale.Translations = locale.Translations.OrderBy(pair => pair.Key).ToDictionary(pair => pair.Key, pair => pair.Value); @@ -53,7 +50,7 @@ namespace Ryujinx.BuildValidationTasks string jsonString = JsonConvert.SerializeObject(json, Formatting.Indented); - using (StreamWriter sw = new StreamWriter(path)) + using (StreamWriter sw = new(path)) { sw.Write(jsonString); } diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 167accd34..33dd61d42 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -21574,4 +21574,4 @@ } } ] -} +} \ No newline at end of file From b5483d8fe028e18fe0ac4a69e40eb3780eaf76e1 Mon Sep 17 00:00:00 2001 From: Marco Carvalho Date: Sun, 22 Dec 2024 16:23:35 -0300 Subject: [PATCH 58/79] Prefer generic overload when type is known (#430) --- src/Ryujinx.Graphics.Vulkan/FormatCapabilities.cs | 2 +- src/Ryujinx.Graphics.Vulkan/FormatTable.cs | 2 +- src/Ryujinx.Graphics.Vulkan/Queries/Counters.cs | 2 +- src/Ryujinx.HLE/HOS/Services/IpcService.cs | 12 ++++++------ .../IUserLocalCommunicationService.cs | 2 +- src/Ryujinx.HLE/HOS/Services/Sm/IUserInterface.cs | 2 +- src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs | 4 ++-- src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs | 2 +- 8 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Ryujinx.Graphics.Vulkan/FormatCapabilities.cs b/src/Ryujinx.Graphics.Vulkan/FormatCapabilities.cs index 4971a63b6..09f22889c 100644 --- a/src/Ryujinx.Graphics.Vulkan/FormatCapabilities.cs +++ b/src/Ryujinx.Graphics.Vulkan/FormatCapabilities.cs @@ -62,7 +62,7 @@ namespace Ryujinx.Graphics.Vulkan _api = api; _physicalDevice = physicalDevice; - int totalFormats = Enum.GetNames(typeof(Format)).Length; + int totalFormats = Enum.GetNames().Length; _bufferTable = new FormatFeatureFlags[totalFormats]; _optimalTable = new FormatFeatureFlags[totalFormats]; diff --git a/src/Ryujinx.Graphics.Vulkan/FormatTable.cs b/src/Ryujinx.Graphics.Vulkan/FormatTable.cs index 98796d9bf..305224cad 100644 --- a/src/Ryujinx.Graphics.Vulkan/FormatTable.cs +++ b/src/Ryujinx.Graphics.Vulkan/FormatTable.cs @@ -12,7 +12,7 @@ namespace Ryujinx.Graphics.Vulkan static FormatTable() { - _table = new VkFormat[Enum.GetNames(typeof(Format)).Length]; + _table = new VkFormat[Enum.GetNames().Length]; _reverseMap = new Dictionary(); #pragma warning disable IDE0055 // Disable formatting diff --git a/src/Ryujinx.Graphics.Vulkan/Queries/Counters.cs b/src/Ryujinx.Graphics.Vulkan/Queries/Counters.cs index 518ede5f3..c07e1c09c 100644 --- a/src/Ryujinx.Graphics.Vulkan/Queries/Counters.cs +++ b/src/Ryujinx.Graphics.Vulkan/Queries/Counters.cs @@ -13,7 +13,7 @@ namespace Ryujinx.Graphics.Vulkan.Queries { _pipeline = pipeline; - int count = Enum.GetNames(typeof(CounterType)).Length; + int count = Enum.GetNames().Length; _counterQueues = new CounterQueue[count]; diff --git a/src/Ryujinx.HLE/HOS/Services/IpcService.cs b/src/Ryujinx.HLE/HOS/Services/IpcService.cs index 808f21c0e..cd3df4edf 100644 --- a/src/Ryujinx.HLE/HOS/Services/IpcService.cs +++ b/src/Ryujinx.HLE/HOS/Services/IpcService.cs @@ -23,18 +23,18 @@ namespace Ryujinx.HLE.HOS.Services public IpcService(ServerBase server = null) { - CmifCommands = typeof(IpcService).Assembly.GetTypes() + CmifCommands = GetType().Assembly.GetTypes() .Where(type => type == GetType()) .SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public)) - .SelectMany(methodInfo => methodInfo.GetCustomAttributes(typeof(CommandCmifAttribute)) - .Select(command => (((CommandCmifAttribute)command).Id, methodInfo))) + .SelectMany(methodInfo => methodInfo.GetCustomAttributes() + .Select(command => (command.Id, methodInfo))) .ToDictionary(command => command.Id, command => command.methodInfo); - TipcCommands = typeof(IpcService).Assembly.GetTypes() + TipcCommands = GetType().Assembly.GetTypes() .Where(type => type == GetType()) .SelectMany(type => type.GetMethods(BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public)) - .SelectMany(methodInfo => methodInfo.GetCustomAttributes(typeof(CommandTipcAttribute)) - .Select(command => (((CommandTipcAttribute)command).Id, methodInfo))) + .SelectMany(methodInfo => methodInfo.GetCustomAttributes() + .Select(command => (command.Id, methodInfo))) .ToDictionary(command => command.Id, command => command.methodInfo); Server = server; diff --git a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs index 9f65aed4b..b8b3014f1 100644 --- a/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs +++ b/src/Ryujinx.HLE/HOS/Services/Ldn/UserServiceCreator/IUserLocalCommunicationService.cs @@ -444,7 +444,7 @@ namespace Ryujinx.HLE.HOS.Services.Ldn.UserServiceCreator private ResultCode ScanInternal(IVirtualMemoryManager memory, ushort channel, ScanFilter scanFilter, ulong bufferPosition, ulong bufferSize, out ulong counter) { - ulong networkInfoSize = (ulong)Marshal.SizeOf(typeof(NetworkInfo)); + ulong networkInfoSize = (ulong)Marshal.SizeOf(); ulong maxGames = bufferSize / networkInfoSize; MemoryHelper.FillWithZeros(memory, bufferPosition, (int)bufferSize); diff --git a/src/Ryujinx.HLE/HOS/Services/Sm/IUserInterface.cs b/src/Ryujinx.HLE/HOS/Services/Sm/IUserInterface.cs index 7a90c664e..271b8fc84 100644 --- a/src/Ryujinx.HLE/HOS/Services/Sm/IUserInterface.cs +++ b/src/Ryujinx.HLE/HOS/Services/Sm/IUserInterface.cs @@ -94,7 +94,7 @@ namespace Ryujinx.HLE.HOS.Services.Sm { if (_services.TryGetValue(name, out Type type)) { - ServiceAttribute serviceAttribute = (ServiceAttribute)type.GetCustomAttributes(typeof(ServiceAttribute)).First(service => ((ServiceAttribute)service).Name == name); + ServiceAttribute serviceAttribute = type.GetCustomAttributes().First(service => service.Name == name); IpcService service = GetServiceInstance(type, context, serviceAttribute.Parameter); diff --git a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs index f11d6e404..493e6659d 100644 --- a/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/Input/InputViewModel.cs @@ -97,7 +97,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input { if (IsModified) { - + _playerIdChoose = value; return; } @@ -105,7 +105,7 @@ namespace Ryujinx.Ava.UI.ViewModels.Input IsModified = false; _playerId = value; - if (!Enum.IsDefined(typeof(PlayerIndex), _playerId)) + if (!Enum.IsDefined(_playerId)) { _playerId = PlayerIndex.Player1; diff --git a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs index dd4ed8297..1b017dbeb 100644 --- a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs @@ -52,7 +52,7 @@ namespace Ryujinx.Ava.UI.Views.Main private void AspectRatioStatus_OnClick(object sender, RoutedEventArgs e) { AspectRatio aspectRatio = ConfigurationState.Instance.Graphics.AspectRatio.Value; - ConfigurationState.Instance.Graphics.AspectRatio.Value = (int)aspectRatio + 1 > Enum.GetNames(typeof(AspectRatio)).Length - 1 ? AspectRatio.Fixed4x3 : aspectRatio + 1; + ConfigurationState.Instance.Graphics.AspectRatio.Value = (int)aspectRatio + 1 > Enum.GetNames().Length - 1 ? AspectRatio.Fixed4x3 : aspectRatio + 1; } private void Refresh_OnClick(object sender, RoutedEventArgs e) => Window.LoadApplications(); From cb355f504d8875defa5f9047dd63b7548e225212 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 22 Dec 2024 16:01:09 -0600 Subject: [PATCH 59/79] UI: Rearrange help menu item & merge wiki page link buttons into a "category" button. --- .../SyscallGenerator.cs | 6 +-- src/Ryujinx/Assets/locales.json | 26 +++++++++- .../UI/Views/Main/MainMenuBarView.axaml | 49 ++++++++++--------- 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/src/Ryujinx.Horizon.Kernel.Generators/SyscallGenerator.cs b/src/Ryujinx.Horizon.Kernel.Generators/SyscallGenerator.cs index 7419a839a..06b98e09d 100644 --- a/src/Ryujinx.Horizon.Kernel.Generators/SyscallGenerator.cs +++ b/src/Ryujinx.Horizon.Kernel.Generators/SyscallGenerator.cs @@ -34,14 +34,14 @@ namespace Ryujinx.Horizon.Kernel.Generators private const string TypeResult = NamespaceHorizonCommon + "." + TypeResultName; private const string TypeExecutionContext = "IExecutionContext"; - private static readonly string[] _expectedResults = new string[] - { + private static readonly string[] _expectedResults = + [ $"{TypeResultName}.Success", $"{TypeKernelResultName}.TimedOut", $"{TypeKernelResultName}.Cancelled", $"{TypeKernelResultName}.PortRemoteClosed", $"{TypeKernelResultName}.InvalidState", - }; + ]; private readonly struct OutParameter { diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 33dd61d42..ee6bf3792 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -1125,6 +1125,30 @@ "zh_TW": "檢查更新" } }, + { + "ID": "MenuBarHelpFaqAndGuides", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "FAQ & Guides", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "MenuBarHelpFaq", "Translations": { @@ -21574,4 +21598,4 @@ } } ] -} \ No newline at end of file +} diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index be805566e..65f048bee 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -290,6 +290,11 @@ + - - - - - + + + + + From 23b0b2240069b3a9b683d6270f39e30ba729cf0c Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Sun, 22 Dec 2024 16:08:12 -0600 Subject: [PATCH 60/79] UI: Ensure last played date & time are always on 2 separate lines, for consistency. --- src/Ryujinx.UI.Common/App/ApplicationData.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx.UI.Common/App/ApplicationData.cs b/src/Ryujinx.UI.Common/App/ApplicationData.cs index 151220f39..657b9a022 100644 --- a/src/Ryujinx.UI.Common/App/ApplicationData.cs +++ b/src/Ryujinx.UI.Common/App/ApplicationData.cs @@ -38,7 +38,7 @@ namespace Ryujinx.UI.App.Common public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed); - public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed) ?? LocalizedNever(); + public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed)?.Replace(" ", "\n") ?? LocalizedNever(); public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize); From a270dc721ccd12873c36d8b766cf8f3483c08a78 Mon Sep 17 00:00:00 2001 From: asfasagag Date: Sun, 22 Dec 2024 20:49:40 -0800 Subject: [PATCH 61/79] UI: Option to resize window to 1440p, 2160p (#432) Minor but useful quality of life addition --- src/Ryujinx/Assets/locales.json | 48 +++++++++++++++++++ .../UI/Views/Main/MainMenuBarView.axaml | 2 + 2 files changed, 50 insertions(+) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index ee6bf3792..a6d0ece6a 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -1077,6 +1077,54 @@ "zh_TW": "" } }, + { + "ID": "MenuBarViewWindow1440", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "1440p", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, + { + "ID": "MenuBarViewWindow2160", + "Translations": { + "ar_SA": "", + "de_DE": "", + "el_GR": "", + "en_US": "2160p", + "es_ES": "", + "fr_FR": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "MenuBarHelp", "Translations": { diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index 65f048bee..7d8135dcf 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -287,6 +287,8 @@ + + From a560d2efdb759bea4c7300985295200980b008ae Mon Sep 17 00:00:00 2001 From: GabCoolGuy Date: Mon, 23 Dec 2024 22:43:06 +0100 Subject: [PATCH 62/79] UI: Added missing french locales/Translated french locales (#415) Custom refresh rate locales and fixed a couple others too --- src/Ryujinx/Assets/locales.json | 114 ++++++++++++++++---------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index a6d0ece6a..c4038eeef 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -701,7 +701,7 @@ "el_GR": "", "en_US": "Scan An Amiibo (From Bin)", "es_ES": "", - "fr_FR": "", + "fr_FR": "Scanner un Amiibo (à partir d'un .bin)", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -749,7 +749,7 @@ "el_GR": "Εγκατάσταση Firmware", "en_US": "Install Firmware", "es_ES": "Instalar firmware", - "fr_FR": "Installer un firmware", + "fr_FR": "Installer le firmware", "he_IL": "התקן קושחה", "it_IT": "Installa firmware", "ja_JP": "ファームウェアをインストール", @@ -821,7 +821,7 @@ "el_GR": "", "en_US": "Install Keys", "es_ES": "", - "fr_FR": "", + "fr_FR": "Installer des clés", "he_IL": "", "it_IT": "Installa Chiavi", "ja_JP": "", @@ -845,7 +845,7 @@ "el_GR": "", "en_US": "Install keys from KEYS or ZIP", "es_ES": "Instalar keys de KEYS o ZIP", - "fr_FR": "", + "fr_FR": "Installer des clés à partir de .KEYS or .ZIP", "he_IL": "", "it_IT": "Installa Chiavi da file KEYS o ZIP", "ja_JP": "", @@ -869,7 +869,7 @@ "el_GR": "", "en_US": "Install keys from a directory", "es_ES": "Instalar keys de un directorio", - "fr_FR": "", + "fr_FR": "Installer des clés à partir d'un dossier", "he_IL": "", "it_IT": "Installa Chiavi da una Cartella", "ja_JP": "", @@ -3221,7 +3221,7 @@ "el_GR": "ΗΠΑ", "en_US": "USA", "es_ES": "EEUU", - "fr_FR": "", + "fr_FR": "États-Unis", "he_IL": "ארה\"ב", "it_IT": "Stati Uniti d'America", "ja_JP": "アメリカ", @@ -3845,7 +3845,7 @@ "el_GR": "", "en_US": "Resync to PC Date & Time", "es_ES": "", - "fr_FR": "", + "fr_FR": "Resynchronier la Date à celle du PC", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -4061,7 +4061,7 @@ "el_GR": "Μικροδιορθώσεις", "en_US": "Hacks", "es_ES": "", - "fr_FR": "", + "fr_FR": "Hacks", "he_IL": "האצות", "it_IT": "Espedienti", "ja_JP": "ハック", @@ -4613,7 +4613,7 @@ "el_GR": "", "en_US": "4x (2880p/4320p) (Not recommended)", "es_ES": "4x (2880p/4320p) (no recomendado)", - "fr_FR": "4x (2880p/4320p) (Non recommandé)", + "fr_FR": "x4 (2880p/4320p) (Non recommandé)", "he_IL": "4x (2880p/4320p) (לא מומלץ)", "it_IT": "4x (2880p/4320p) (Non consigliato)", "ja_JP": "4x (2880p/4320p) (非推奨)", @@ -4637,7 +4637,7 @@ "el_GR": "Αναλογία Απεικόνισης:", "en_US": "Aspect Ratio:", "es_ES": "Relación de aspecto:", - "fr_FR": "Format d'affichage :", + "fr_FR": "Format d'affichage :", "he_IL": "יחס גובה-רוחב:", "it_IT": "Rapporto d'aspetto:", "ja_JP": "アスペクト比:", @@ -10301,7 +10301,7 @@ "el_GR": "", "en_US": "View Profile", "es_ES": "Ver perfil", - "fr_FR": "", + "fr_FR": "Voir Profil", "he_IL": "", "it_IT": "Visualizza profilo", "ja_JP": "", @@ -11237,20 +11237,20 @@ "el_GR": "Αποτυχία μετατροπής της ληφθείσας έκδοσης Ryujinx από την έκδοση GitHub.", "en_US": "Failed to convert the Ryujinx version received from GitHub.", "es_ES": "No se pudo convertir la versión de Ryujinx recibida de GitHub Release.", - "fr_FR": "Impossible de convertir la version reçue de Ryujinx depuis Github Release.", + "fr_FR": "Impossible de convertir la version reçue de Ryujinx depuis GitHub Release.", "he_IL": "המרת גרסת ריוג'ינקס שהתקבלה מ-עדכון הגרסאות של גיטהב נכשלה.", - "it_IT": "La conversione della versione di Ryujinx ricevuta da Github Release è fallita.", + "it_IT": "La conversione della versione di Ryujinx ricevuta da GitHub Release è fallita.", "ja_JP": "Github から取得した Ryujinx バージョンの変換に失敗しました.", "ko_KR": "GitHub에서 받은 Ryujinx 버전을 변환하지 못했습니다.", - "no_NO": "Kan ikke konvertere mottatt Ryujinx-versjon fra Github Utgivelse.", + "no_NO": "Kan ikke konvertere mottatt Ryujinx-versjon fra GitHub Utgivelse.", "pl_PL": "Nie udało się przekonwertować otrzymanej wersji Ryujinx z Github Release.", "pt_BR": "Falha ao converter a versão do Ryujinx recebida do AppVeyor.", - "ru_RU": "Не удалось преобразовать полученную версию Ryujinx из Github Release.", - "th_TH": "ไม่สามารถแปลงเวอร์ชั่น Ryujinx ที่ได้รับจาก Github Release", + "ru_RU": "Не удалось преобразовать полученную версию Ryujinx из GitHub Release.", + "th_TH": "ไม่สามารถแปลงเวอร์ชั่น Ryujinx ที่ได้รับจาก GitHub Release", "tr_TR": "Github Release'den alınan Ryujinx sürümü dönüştürülemedi.", - "uk_UA": "Не вдалося конвертувати отриману версію Ryujinx із випуску Github.", - "zh_CN": "无法切换至从 Github 接收到的新版 Ryujinx 模拟器。", - "zh_TW": "無法轉換從 Github Release 接收到的 Ryujinx 版本。" + "uk_UA": "Не вдалося конвертувати отриману версію Ryujinx із випуску GitHub.", + "zh_CN": "无法切换至从 GitHub 接收到的新版 Ryujinx 模拟器。", + "zh_TW": "無法轉換從 GitHub Release 接收到的 Ryujinx 版本。" } }, { @@ -11357,7 +11357,7 @@ "el_GR": "", "en_US": "Show Changelog", "es_ES": "", - "fr_FR": "", + "fr_FR": "Afficher Changelog", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -12317,7 +12317,7 @@ "el_GR": "Σφάλμα UI: Το επιλεγμένο παιχνίδι δεν έχει έγκυρο αναγνωριστικό τίτλου", "en_US": "UI error: The selected game did not have a valid title ID", "es_ES": "Error de interfaz: El juego seleccionado no tiene una ID válida", - "fr_FR": "Erreur d'UI : le jeu sélectionné n'a pas d'ID de titre valide", + "fr_FR": "Erreur d'UI : Le jeu sélectionné n'a pas d'ID de titre valide", "he_IL": "שגיאת ממשק משתמש: למשחק שנבחר לא קיים מזהה משחק", "it_IT": "Errore UI: Il gioco selezionato non ha un ID titolo valido", "ja_JP": "UI エラー: 選択されたゲームは有効なタイトル ID を保持していません", @@ -12509,7 +12509,7 @@ "el_GR": "", "en_US": "An invalid Keys file was found in {0}", "es_ES": "Se halló un archivo Keys inválido en {0}", - "fr_FR": "", + "fr_FR": "Un fichier de clés invalide a été trouvé dans {0}", "he_IL": "", "it_IT": "E' stato trovato un file di chiavi invalido ' {0}", "ja_JP": "", @@ -12533,7 +12533,7 @@ "el_GR": "", "en_US": "Install Keys", "es_ES": "Instalar Keys", - "fr_FR": "", + "fr_FR": "Installer des clés", "he_IL": "", "it_IT": "Installa Chavi", "ja_JP": "", @@ -12557,7 +12557,7 @@ "el_GR": "", "en_US": "New Keys file will be installed.", "es_ES": "Un nuevo archivo Keys será instalado.", - "fr_FR": "", + "fr_FR": "Nouveau fichier de clés sera installé.", "he_IL": "", "it_IT": "Un nuovo file di Chiavi sarà intallato.", "ja_JP": "", @@ -12581,7 +12581,7 @@ "el_GR": "", "en_US": "\n\nThis may replace some of the current installed Keys.", "es_ES": "\n\nEsto puede reemplazar algunas de las Keys actualmente instaladas.", - "fr_FR": "", + "fr_FR": "\n\nCela pourrait remplacer les clés qui sont installés.", "he_IL": "", "it_IT": "\n\nQuesto potrebbe sovrascrivere alcune delle Chiavi già installate.", "ja_JP": "", @@ -12605,7 +12605,7 @@ "el_GR": "", "en_US": "\n\nDo you want to continue?", "es_ES": "\n\nDeseas continuar?", - "fr_FR": "", + "fr_FR": "\n\nVoulez-vous continuez ?", "he_IL": "", "it_IT": "\n\nVuoi continuare?", "ja_JP": "", @@ -12629,7 +12629,7 @@ "el_GR": "", "en_US": "Installing Keys...", "es_ES": "Instalando Keys...", - "fr_FR": "", + "fr_FR": "Installation des clés...", "he_IL": "", "it_IT": "Installando le chiavi...", "ja_JP": "", @@ -12653,7 +12653,7 @@ "el_GR": "", "en_US": "New Keys file successfully installed.", "es_ES": "Nuevo archivo Keys instalado con éxito.", - "fr_FR": "", + "fr_FR": "Nouveau fichier de clés a été installé.", "he_IL": "", "it_IT": "Nuovo file di chiavi installato con successo.", "ja_JP": "", @@ -13613,7 +13613,7 @@ "el_GR": "", "en_US": "Ryujinx is an emulator for the Nintendo Switch™.\nGet all the latest news in our Discord.\nDevelopers interested in contributing can find out more on our GitHub or Discord.", "es_ES": "", - "fr_FR": "", + "fr_FR": "Ryujinx est un émulateur pour la Nintendo Switch™.\nObtenez le dernières nouvelles sur le Discord.\nLes développeurs qui veulent contribuer peuvent en savoir plus sur notre GitHub ou Discord.", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -13661,7 +13661,7 @@ "el_GR": "", "en_US": "Formerly Maintained By:", "es_ES": "", - "fr_FR": "", + "fr_FR": "Anciennement Maintenu par :", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -14501,7 +14501,7 @@ "el_GR": "", "en_US": "Direct keyboard access (HID) support. Provides games access to your keyboard as a text entry device.\n\nOnly works with games that natively support keyboard usage on Switch hardware.\n\nLeave OFF if unsure.", "es_ES": "Soporte de acceso directo al teclado (HID). Proporciona a los juegos acceso a su teclado como dispositivo de entrada de texto.\n\nSolo funciona con juegos que permiten de forma nativa el uso del teclado en el hardware de Switch.\n\nDesactívalo si no sabes qué hacer.", - "fr_FR": "Prise en charge de l'accès direct au clavier (HID). Permet aux jeux d'accéder à votre clavier comme périphérique de saisie de texte.\n\nFonctionne uniquement avec les jeux prenant en charge nativement l'utilisation du clavier sur le matériel Switch.\n\nLaissez OFF si vous n'êtes pas sûr.", + "fr_FR": "Prise en charge de l'accès direct au clavier (HID). Permet aux jeux d'accéder à votre clavier comme périphérique de saisie de texte.\n\nFonctionne uniquement avec les jeux prenant en charge nativement l'utilisation du clavier sur le matériel Switch.\n\nLaissez désactiver si vous n'êtes pas sûr.", "he_IL": "", "it_IT": "Supporto per l'accesso diretto alla tastiera (HID). Fornisce ai giochi l'accesso alla tastiera come dispositivo di inserimento del testo.\n\nFunziona solo con i giochi che supportano nativamente l'utilizzo della tastiera su hardware Switch.\n\nNel dubbio, lascia l'opzione disattivata.", "ja_JP": "直接キーボード アクセス (HID) のサポートです. テキスト入力デバイスとしてキーボードへのゲームアクセスを提供します.\n\nSwitchハードウェアでキーボードの使用をネイティブにサポートしているゲームでのみ動作します.\n\nわからない場合はオフのままにしてください.", @@ -14525,7 +14525,7 @@ "el_GR": "", "en_US": "Direct mouse access (HID) support. Provides games access to your mouse as a pointing device.\n\nOnly works with games that natively support mouse controls on Switch hardware, which are few and far between.\n\nWhen enabled, touch screen functionality may not work.\n\nLeave OFF if unsure.", "es_ES": "Soporte de acceso directo al mouse (HID). Proporciona a los juegos acceso a su mouse como puntero.\n\nSolo funciona con juegos que permiten de forma nativa el uso de controles con mouse en el hardware de switch, lo cual son pocos.\n\nCuando esté activado, la funcionalidad de pantalla táctil puede no funcionar.\n\nDesactívalo si no sabes qué hacer.", - "fr_FR": "Prise en charge de l'accès direct à la souris (HID). Permet aux jeux d'accéder à votre souris en tant que dispositif de pointage.\n\nFonctionne uniquement avec les jeux qui prennent en charge nativement les contrôles de souris sur le matériel Switch, ce qui est rare.\n\nLorsqu'il est activé, la fonctionnalité de l'écran tactile peut ne pas fonctionner.\n\nLaissez sur OFF si vous n'êtes pas sûr.", + "fr_FR": "Prise en charge de l'accès direct à la souris (HID). Permet aux jeux d'accéder à votre souris en tant que dispositif de pointage.\n\nFonctionne uniquement avec les jeux qui prennent en charge nativement les contrôles de souris sur le matériel Switch, ce qui est rare.\n\nLorsqu'il est activé, la fonctionnalité de l'écran tactile peut ne pas fonctionner.\n\nLaissez désactiver si vous n'êtes pas sûr.", "he_IL": "", "it_IT": "Supporto per l'accesso diretto al mouse (HID). Fornisce ai giochi l'accesso al mouse come dispositivo di puntamento.\n\nFunziona solo con i rari giochi che supportano nativamente l'utilizzo del mouse su hardware Switch.\n\nQuando questa opzione è attivata, il touchscreen potrebbe non funzionare.\n\nNel dubbio, lascia l'opzione disattivata.", "ja_JP": "直接マウスアクセス (HID) のサポートです. ポインティングデバイスとしてマウスへのゲームアクセスを提供します.\n\nSwitchハードウェアでマウスの使用をネイティブにサポートしているゲームでのみ動作します.\n\n有効にしている場合, タッチスクリーン機能は動作しない場合があります.\n\nわからない場合はオフのままにしてください.", @@ -14645,7 +14645,7 @@ "el_GR": "", "en_US": "Resync System Time to match your PC's current date & time.\n\nThis is not an active setting, it can still fall out of sync; in which case just click this button again.", "es_ES": "", - "fr_FR": "", + "fr_FR": "Resynchronise la Date du Système pour qu'elle soit la même que celle du PC.\n\nCeci n'est pas un paramètrage automatique, la date peut se désynchroniser; dans ce cas là, rappuyer sur le boutton.", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -16037,7 +16037,7 @@ "el_GR": "Εύρος:", "en_US": "Range:", "es_ES": "Alcance:", - "fr_FR": "Intervalle :", + "fr_FR": "Intervalle :", "he_IL": "טווח:", "it_IT": "Raggio:", "ja_JP": "範囲:", @@ -17093,7 +17093,7 @@ "el_GR": "", "en_US": "Cabinet Dialog", "es_ES": "Diálogo Gabinete", - "fr_FR": "", + "fr_FR": "Dialogue de Cabinet", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -17117,7 +17117,7 @@ "el_GR": "", "en_US": "Enter your Amiibo's new name", "es_ES": "Ingresa el nuevo nombre de tu Amiibo", - "fr_FR": "", + "fr_FR": "Entrer le nouveau nom de votre Amiibo", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -17141,7 +17141,7 @@ "el_GR": "", "en_US": "Please scan your Amiibo now.", "es_ES": "Escanea tu Amiibo ahora.", - "fr_FR": "", + "fr_FR": "Veuillez scannez votre Amiibo.", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -19133,7 +19133,7 @@ "el_GR": "", "en_US": "{0} DLC(s) available", "es_ES": "", - "fr_FR": "", + "fr_FR": "{0} DLC(s) disponibles", "he_IL": "{0} הרחבות משחק", "it_IT": "", "ja_JP": "", @@ -19589,7 +19589,7 @@ "el_GR": "Όνομα:", "en_US": "Name:", "es_ES": "Nombre:", - "fr_FR": "Nom :", + "fr_FR": "Nom :", "he_IL": "שם:", "it_IT": "Nome:", "ja_JP": "名称:", @@ -21269,7 +21269,7 @@ "el_GR": "", "en_US": "VSync:", "es_ES": "", - "fr_FR": "", + "fr_FR": "VSync :", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -21293,7 +21293,7 @@ "el_GR": "", "en_US": "Enable custom refresh rate (Experimental)", "es_ES": "", - "fr_FR": "", + "fr_FR": "Activer le taux de rafraîchissement customisé (Expérimental)", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -21317,7 +21317,7 @@ "el_GR": "", "en_US": "Switch", "es_ES": "", - "fr_FR": "", + "fr_FR": "Switch", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -21341,7 +21341,7 @@ "el_GR": "", "en_US": "Unbounded", "es_ES": "", - "fr_FR": "", + "fr_FR": "Sans Limite", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -21365,7 +21365,7 @@ "el_GR": "", "en_US": "Custom Refresh Rate", "es_ES": "", - "fr_FR": "", + "fr_FR": "Taux de Rafraîchissement Customisé", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -21389,7 +21389,7 @@ "el_GR": "", "en_US": "Emulated Vertical Sync. 'Switch' emulates the Switch's refresh rate of 60Hz. 'Unbounded' is an unbounded refresh rate.", "es_ES": "", - "fr_FR": "", + "fr_FR": "VSync émulé. 'Switch' émule le taux de rafraîchissement de la Switch (60Hz). 'Sans Limite' est un taux de rafraîchissement qui n'est pas limité.", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -21411,9 +21411,9 @@ "ar_SA": "", "de_DE": "", "el_GR": "", - "en_US": "Emulated Vertical Sync. 'Switch' emulates the Switch's refresh rate of 60Hz. 'Unbounded' is an unbounded refresh rate. 'Custom' emulates the specified custom refresh rate.", + "en_US": "Emulated Vertical Sync. 'Switch' emulates the Switch's refresh rate of 60Hz. 'Unbounded' is an unbounded refresh rate. 'Custom Refresh Rate' emulates the specified custom refresh rate.", "es_ES": "", - "fr_FR": "", + "fr_FR": "VSync émulé. 'Switch' émule le taux de rafraîchissement de la Switch (60Hz). 'Sans Limite' est un taux de rafraîchissement qui n'est pas limité. 'Taux de Rafraîchissement Customisé' émule le taux de rafraîchissement spécifié.", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -21437,7 +21437,7 @@ "el_GR": "", "en_US": "Allows the user to specify an emulated refresh rate. In some titles, this may speed up or slow down the rate of gameplay logic. In other titles, it may allow for capping FPS at some multiple of the refresh rate, or lead to unpredictable behavior. This is an experimental feature, with no guarantees for how gameplay will be affected. \n\nLeave OFF if unsure.", "es_ES": "", - "fr_FR": "", + "fr_FR": "Permet à l'utilisateur de spécifier un taux de rafraîchissement émulé. Dans certains jeux, ceci pourrait accélérer ou ralentir le taux de logique du gameplay. Dans d'autre titres, cela permettrait limiter le FPS à un multiple du taux de rafraîchissement, ou conduire à un comportement imprévisible. Ceci est une fonctionnalité expérimentale, avec aucune garanties pour comment le gameplay sera affecté. \n\nLaisser désactiver en cas de doute.", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -21461,7 +21461,7 @@ "el_GR": "", "en_US": "The custom refresh rate target value.", "es_ES": "", - "fr_FR": "", + "fr_FR": "La valeur cible du taux de rafraîchissement customisé.", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -21485,7 +21485,7 @@ "el_GR": "", "en_US": "The custom refresh rate, as a percentage of the normal Switch refresh rate.", "es_ES": "", - "fr_FR": "", + "fr_FR": "Le taux de rafraîchissement customisé, comme un pourcentage du taux de rafraîchissement normal de la Switch.", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -21509,7 +21509,7 @@ "el_GR": "", "en_US": "Custom Refresh Rate %:", "es_ES": "", - "fr_FR": "", + "fr_FR": "Pourcentage du Taux de Rafraîchissement Customisé :", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -21533,7 +21533,7 @@ "el_GR": "", "en_US": "Custom Refresh Rate Value:", "es_ES": "", - "fr_FR": "", + "fr_FR": "Valeur du Taux de Rafraîchissement Customisé :", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -21557,7 +21557,7 @@ "el_GR": "", "en_US": "Interval", "es_ES": "", - "fr_FR": "", + "fr_FR": "Intervalle", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -21581,7 +21581,7 @@ "el_GR": "", "en_US": "Toggle VSync mode:", "es_ES": "", - "fr_FR": "", + "fr_FR": "Activer/Désactiver mode VSync :", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -21605,7 +21605,7 @@ "el_GR": "", "en_US": "Raise custom refresh rate", "es_ES": "", - "fr_FR": "", + "fr_FR": "Augmenter le taux de rafraîchissement customisé :", "he_IL": "", "it_IT": "", "ja_JP": "", @@ -21627,9 +21627,9 @@ "ar_SA": "", "de_DE": "", "el_GR": "", - "en_US": "Lower custom refresh rate", + "en_US": "Lower custom refresh rate:", "es_ES": "", - "fr_FR": "", + "fr_FR": "Baisser le taux de rafraîchissement customisé :", "he_IL": "", "it_IT": "", "ja_JP": "", From 278fe9d4f040585f0683f0f360f95c22510be302 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Mon, 23 Dec 2024 16:16:48 -0600 Subject: [PATCH 63/79] Add buildvalidationtasks project to infra label --- .github/labeler.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/labeler.yml b/.github/labeler.yml index 871f9945f..ac3c77288 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -32,7 +32,7 @@ kernel: infra: - changed-files: - - any-glob-to-any-file: ['.github/**', 'distribution/**', 'Directory.Packages.props'] + - any-glob-to-any-file: ['.github/**', 'distribution/**', 'Directory.Packages.props', 'src/Ryujinx.BuildValidationTasks/**'] documentation: - changed-files: From 3094df54dd9ad8335dbf04804f01c1eec5d72b20 Mon Sep 17 00:00:00 2001 From: Daenorth Date: Tue, 24 Dec 2024 00:03:33 +0100 Subject: [PATCH 64/79] Update Norwegian Translation (#418) swiggybobo --- src/Ryujinx/Assets/locales.json | 300 ++++++++++++++++---------------- 1 file changed, 150 insertions(+), 150 deletions(-) diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index c4038eeef..2e86df0aa 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -82,7 +82,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "Mii 편집 애플릿", - "no_NO": "", + "no_NO": "Mii-redigeringsapplet", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -370,7 +370,7 @@ "it_IT": "Carica DLC Da una Cartella", "ja_JP": "", "ko_KR": "폴더에서 DLC 불러오기", - "no_NO": "", + "no_NO": "Last inn DLC fra mappe", "pl_PL": "", "pt_BR": "Carregar DLC da Pasta", "ru_RU": "", @@ -394,7 +394,7 @@ "it_IT": "Carica Aggiornamenti Da una Cartella", "ja_JP": "", "ko_KR": "폴더에서 타이틀 업데이트 불러오기", - "no_NO": "", + "no_NO": "Last inn titteloppdateringer fra mappe", "pl_PL": "", "pt_BR": "Carregar Atualizações de Jogo da Pasta", "ru_RU": "", @@ -706,7 +706,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "Amiibo 스캔(빈에서)", - "no_NO": "", + "no_NO": "Skann en Amiibo (fra bin fil)", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -826,7 +826,7 @@ "it_IT": "Installa Chiavi", "ja_JP": "", "ko_KR": "설치 키", - "no_NO": "", + "no_NO": "Installere nøkler", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -850,7 +850,7 @@ "it_IT": "Installa Chiavi da file KEYS o ZIP", "ja_JP": "", "ko_KR": "키나 ZIP에서 키 설치", - "no_NO": "", + "no_NO": "Installer nøkler fra KEYS eller ZIP", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -874,7 +874,7 @@ "it_IT": "Installa Chiavi da una Cartella", "ja_JP": "", "ko_KR": "디렉터리에서 키 설치", - "no_NO": "", + "no_NO": "Installer nøkler fra en mappe", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -970,7 +970,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "XCI 파일 트리머", - "no_NO": "", + "no_NO": "Trim XCI-filer", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -1042,7 +1042,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "", - "no_NO": "", + "no_NO": "720p", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -1066,7 +1066,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "", - "no_NO": "", + "no_NO": "1080p", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -1210,7 +1210,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "자주 묻는 질문(FAQ) 및 문제해결 페이지", - "no_NO": "", + "no_NO": "FAQ- og feilsøkingsside", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -1234,7 +1234,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "공식 Ryujinx 위키에서 자주 묻는 질문(FAQ) 및 문제 해결 페이지 열기", - "no_NO": "", + "no_NO": "Åpner FAQ- og feilsøkingssiden på den offisielle Ryujinx-wikien", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -1258,7 +1258,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "설치 및 구성 안내", - "no_NO": "", + "no_NO": "Oppsett- og konfigurasjonsveiledning", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -1282,7 +1282,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "공식 Ryujinx 위키에서 설정 및 구성 안내 열기", - "no_NO": "", + "no_NO": "Åpner oppsett- og konfigurasjonsveiledningen på den offisielle Ryujinx-wikien", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -1306,7 +1306,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "멀티플레이어(LDN/LAN) 안내", - "no_NO": "", + "no_NO": "Flerspillerveiledning (LDN/LAN)", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -1330,7 +1330,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "공식 Ryujinx 위키에서 멀티플레이어 안내 열기", - "no_NO": "", + "no_NO": "Åpner flerspillerveiledningen på den offisielle Ryujinx-wikien", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -2122,7 +2122,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "", - "no_NO": "", + "no_NO": "ExeFS", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -2170,7 +2170,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "", - "no_NO": "", + "no_NO": "RomFS", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -2218,7 +2218,7 @@ "it_IT": "", "ja_JP": "ロゴ", "ko_KR": "로고", - "no_NO": "", + "no_NO": "Logo", "pl_PL": "", "pt_BR": "", "ru_RU": "Логотип", @@ -2434,7 +2434,7 @@ "it_IT": "Controlla e Trimma i file XCI", "ja_JP": "", "ko_KR": "XCI 파일 확인 및 트림", - "no_NO": "", + "no_NO": "Kontroller og trim XCI-filen", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -2458,7 +2458,7 @@ "it_IT": "Controlla e Trimma i file XCI da Salvare Sullo Spazio del Disco", "ja_JP": "", "ko_KR": "디스크 공간을 절약하기 위해 XCI 파일 확인 및 트림", - "no_NO": "", + "no_NO": "Kontroller og trimm XCI-filen for å spare diskplass", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -2530,7 +2530,7 @@ "it_IT": "Trimmando i file XCI '{0}'", "ja_JP": "", "ko_KR": "XCI 파일 '{0}' 트리밍", - "no_NO": "", + "no_NO": "Trimming av XCI-filen '{0}'", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -2890,7 +2890,7 @@ "it_IT": "Mostra barra del titolo (Richiede il riavvio)", "ja_JP": "", "ko_KR": "제목 표시줄 표시(다시 시작해야 함)", - "no_NO": "", + "no_NO": "Vis tittellinje (krever omstart)", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -3034,7 +3034,7 @@ "it_IT": "Directory di Caricamento Automatico per DLC/Aggiornamenti", "ja_JP": "", "ko_KR": "DLC/업데이트 디렉터리 자동 불러오기", - "no_NO": "", + "no_NO": "Autoload DLC/Updates-mapper", "pl_PL": "", "pt_BR": "Carregar Automaticamente Diretórios de DLC/Atualizações", "ru_RU": "", @@ -3058,7 +3058,7 @@ "it_IT": "Aggiornamenti e DLC che collegano a file mancanti verranno disabilitati automaticamente", "ja_JP": "", "ko_KR": "누락된 파일을 참조하는 DLC 및 업데이트가 자동으로 언로드", - "no_NO": "", + "no_NO": "DLC og oppdateringer som henviser til manglende filer, vil bli lastet ned automatisk", "pl_PL": "", "pt_BR": "DLCs e Atualizações que se referem a arquivos ausentes serão descarregadas automaticamente", "ru_RU": "", @@ -3130,7 +3130,7 @@ "it_IT": "Sistema", "ja_JP": "システム", "ko_KR": "시스템", - "no_NO": "", + "no_NO": "System", "pl_PL": "", "pt_BR": "Sistema", "ru_RU": "Система", @@ -3202,7 +3202,7 @@ "it_IT": "Giappone", "ja_JP": "日本", "ko_KR": "일본", - "no_NO": "", + "no_NO": "Japan", "pl_PL": "Japonia", "pt_BR": "Japão", "ru_RU": "Япония", @@ -3226,7 +3226,7 @@ "it_IT": "Stati Uniti d'America", "ja_JP": "アメリカ", "ko_KR": "미국", - "no_NO": "", + "no_NO": "USA", "pl_PL": "Stany Zjednoczone", "pt_BR": "EUA", "ru_RU": "США", @@ -3274,7 +3274,7 @@ "it_IT": "", "ja_JP": "オーストラリア", "ko_KR": "호주", - "no_NO": "", + "no_NO": "Australia", "pl_PL": "", "pt_BR": "Austrália", "ru_RU": "Австралия", @@ -3322,7 +3322,7 @@ "it_IT": "Corea", "ja_JP": "韓国", "ko_KR": "한국", - "no_NO": "", + "no_NO": "Korea", "pl_PL": "", "pt_BR": "Coreia", "ru_RU": "Корея", @@ -3346,7 +3346,7 @@ "it_IT": "", "ja_JP": "台湾", "ko_KR": "대만", - "no_NO": "", + "no_NO": "Taiwan", "pl_PL": "Tajwan", "pt_BR": "", "ru_RU": "Тайвань", @@ -3898,7 +3898,7 @@ "it_IT": "Low-power PPTC", "ja_JP": "Low-power PPTC", "ko_KR": "저전력 PPTC 캐시", - "no_NO": "", + "no_NO": "PPTC med lavt strømforbruk", "pl_PL": "Low-power PPTC", "pt_BR": "Low-power PPTC", "ru_RU": "Low-power PPTC", @@ -3970,7 +3970,7 @@ "it_IT": "", "ja_JP": "ダミー", "ko_KR": "더미", - "no_NO": "", + "no_NO": "Dummy", "pl_PL": "Atrapa", "pt_BR": "Nenhuma", "ru_RU": "Без звука", @@ -3994,7 +3994,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "", - "no_NO": "", + "no_NO": "OpenAL", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -4042,7 +4042,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "", - "no_NO": "", + "no_NO": "SDL2", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -4066,7 +4066,7 @@ "it_IT": "Espedienti", "ja_JP": "ハック", "ko_KR": "핵", - "no_NO": "", + "no_NO": "Hacks", "pl_PL": "Hacki", "pt_BR": "", "ru_RU": "Хаки", @@ -4114,7 +4114,7 @@ "it_IT": "Usa layout di memoria alternativo (per sviluppatori)", "ja_JP": "DRAMサイズ:", "ko_KR": "DRAM 크기 :", - "no_NO": "", + "no_NO": "DRAM Mengde", "pl_PL": "Użyj alternatywnego układu pamięci (Deweloperzy)", "pt_BR": "Tamanho da DRAM:", "ru_RU": "Использовать альтернативный макет памяти (для разработчиков)", @@ -4138,7 +4138,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "4GB", - "no_NO": "", + "no_NO": "4GB", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -4162,7 +4162,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "6GB", - "no_NO": "", + "no_NO": "6GB", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -4186,7 +4186,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "8GB", - "no_NO": "", + "no_NO": "8GB", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -4210,7 +4210,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "12GB", - "no_NO": "", + "no_NO": "12GB", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -4258,7 +4258,7 @@ "it_IT": "Ignora l'applet", "ja_JP": "アプレットを無視する", "ko_KR": "애플릿 무시", - "no_NO": "", + "no_NO": "Ignorer appleten", "pl_PL": "Ignoruj ​​aplet", "pt_BR": "Ignorar applet", "ru_RU": "Игнорировать Апплет", @@ -4402,7 +4402,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "2배", - "no_NO": "", + "no_NO": "2x", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -4426,7 +4426,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "4배", - "no_NO": "", + "no_NO": "4x", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -4450,7 +4450,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "8배", - "no_NO": "", + "no_NO": "8x", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -4474,7 +4474,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "16배", - "no_NO": "", + "no_NO": "16x", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -4570,7 +4570,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "2배(1440p/2160p)", - "no_NO": "2 x (1440p/2160p)", + "no_NO": "2x (1440p/2160p)", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -4594,7 +4594,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "3배(2160p/3240p)", - "no_NO": "", + "no_NO": "3x (2160p/3240p)", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -4666,7 +4666,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "", - "no_NO": "", + "no_NO": "4:3", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -4690,7 +4690,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "", - "no_NO": "", + "no_NO": "16:9", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -4714,7 +4714,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "", - "no_NO": "", + "no_NO": "16:10", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -4738,7 +4738,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "", - "no_NO": "", + "no_NO": "21:9", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -4762,7 +4762,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "", - "no_NO": "", + "no_NO": "32:9", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -4858,7 +4858,7 @@ "it_IT": "Log", "ja_JP": "ロギング", "ko_KR": "로그 기록", - "no_NO": "", + "no_NO": "Logging", "pl_PL": "Dziennik zdarzeń", "pt_BR": "Log", "ru_RU": "Журналирование", @@ -4882,7 +4882,7 @@ "it_IT": "Log", "ja_JP": "ロギング", "ko_KR": "로그 기록", - "no_NO": "", + "no_NO": "Logging", "pl_PL": "Dziennik zdarzeń", "pt_BR": "Log", "ru_RU": "Журналирование", @@ -5434,7 +5434,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "확인", - "no_NO": "", + "no_NO": "OK", "pl_PL": "", "pt_BR": "", "ru_RU": "Ок", @@ -6106,7 +6106,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "", - "no_NO": "", + "no_NO": "A", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -6130,7 +6130,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "", - "no_NO": "", + "no_NO": "B", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -6154,7 +6154,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "", - "no_NO": "", + "no_NO": "X", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -6178,7 +6178,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "", - "no_NO": "", + "no_NO": "Y", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -9130,7 +9130,7 @@ "it_IT": "Pulsante levetta destra", "ja_JP": "", "ko_KR": "우측 스틱 버튼", - "no_NO": "R Styrespak Trykk", + "no_NO": "Høyre Styrespak Trykk", "pl_PL": "", "pt_BR": "", "ru_RU": "Кнопка пр. стика", @@ -10090,7 +10090,7 @@ "it_IT": "Cancellando", "ja_JP": "", "ko_KR": "취소하기", - "no_NO": "", + "no_NO": "Kansellerer", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -10114,7 +10114,7 @@ "it_IT": "Chiudi", "ja_JP": "", "ko_KR": "닫기", - "no_NO": "", + "no_NO": "Lukk", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -10306,7 +10306,7 @@ "it_IT": "Visualizza profilo", "ja_JP": "", "ko_KR": "프로필 보기", - "no_NO": "", + "no_NO": "Se Profil", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -11362,7 +11362,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "변경 로그 보기", - "no_NO": "", + "no_NO": "Vis endringslogg", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -11818,7 +11818,7 @@ "it_IT": "Finestra XCI Trimmer", "ja_JP": "", "ko_KR": "XCI 트리머 창", - "no_NO": "", + "no_NO": "XCI Trimmervindu", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -12514,7 +12514,7 @@ "it_IT": "E' stato trovato un file di chiavi invalido ' {0}", "ja_JP": "", "ko_KR": "{0}에서 잘못된 키 파일이 발견", - "no_NO": "", + "no_NO": "En ugyldig Keys-fil ble funnet i {0}.", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -12538,7 +12538,7 @@ "it_IT": "Installa Chavi", "ja_JP": "", "ko_KR": "설치 키", - "no_NO": "", + "no_NO": "Installere nøkler", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -12562,7 +12562,7 @@ "it_IT": "Un nuovo file di Chiavi sarà intallato.", "ja_JP": "", "ko_KR": "새로운 키 파일이 설치됩니다.", - "no_NO": "", + "no_NO": "Ny Keys-fil vil bli installert.", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -12586,7 +12586,7 @@ "it_IT": "\n\nQuesto potrebbe sovrascrivere alcune delle Chiavi già installate.", "ja_JP": "", "ko_KR": "\n\n이로 인해 현재 설치된 키 중 일부가 대체될 수 있습니다.", - "no_NO": "", + "no_NO": "\n\nDette kan erstatte noen av de nåværende installerte nøklene.", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -12610,7 +12610,7 @@ "it_IT": "\n\nVuoi continuare?", "ja_JP": "", "ko_KR": "\n\n계속하시겠습니까?", - "no_NO": "", + "no_NO": "\n\nVil du fortsette?", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -12634,7 +12634,7 @@ "it_IT": "Installando le chiavi...", "ja_JP": "", "ko_KR": "키 설치 중...", - "no_NO": "", + "no_NO": "Installere nøkler...", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -12658,7 +12658,7 @@ "it_IT": "Nuovo file di chiavi installato con successo.", "ja_JP": "", "ko_KR": "새로운 키 파일이 성공적으로 설치되었습니다.", - "no_NO": "", + "no_NO": "Ny Keys -fil installert.", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -13666,7 +13666,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "이전 관리자 :", - "no_NO": "", + "no_NO": "Tidligere vedlikeholdt av:", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -14338,7 +14338,7 @@ "it_IT": "Inserisci una directory di \"autoload\" da aggiungere alla lista", "ja_JP": "", "ko_KR": "목록에 추가할 자동 불러오기 디렉터리를 입력", - "no_NO": "", + "no_NO": "Angi en autoload-mappe som skal legges til i listen", "pl_PL": "", "pt_BR": "Insira um diretório de carregamento automático para adicionar à lista", "ru_RU": "", @@ -14362,7 +14362,7 @@ "it_IT": "Aggiungi una directory di \"autoload\" alla lista", "ja_JP": "", "ko_KR": "목록에 자동 불러오기 디렉터리 추가", - "no_NO": "", + "no_NO": "Legg til en autoload-mappe i listen", "pl_PL": "", "pt_BR": "Adicionar um diretório de carregamento automático à lista", "ru_RU": "", @@ -14386,7 +14386,7 @@ "it_IT": "Rimuovi la directory di autoload selezionata", "ja_JP": "", "ko_KR": "선택한 자동 불러오기 디렉터리 제거", - "no_NO": "", + "no_NO": "Fjern valgt autoload-mappe", "pl_PL": "", "pt_BR": "Remover o diretório de carregamento automático selecionado", "ru_RU": "", @@ -14722,7 +14722,7 @@ "it_IT": "Carica il PPTC usando un terzo dei core.", "ja_JP": "", "ko_KR": "코어의 3분의 1을 사용하여 PPTC를 불러옵니다.", - "no_NO": "", + "no_NO": "Last inn PPTC med en tredjedel av antall kjerner.", "pl_PL": "", "pt_BR": "Carregar o PPTC usando um terço da quantidade de núcleos.", "ru_RU": "", @@ -14962,7 +14962,7 @@ "it_IT": "La finestra di dialogo esterna \"Controller Applet\" non apparirà se il gamepad viene disconnesso durante il gioco. Non ci sarà alcun prompt per chiudere la finestra di dialogo o impostare un nuovo controller. Una volta che il controller disconnesso in precedenza viene ricollegato, il gioco riprenderà automaticamente.", "ja_JP": "ゲームプレイ中にゲームパッドが切断された場合、外部ダイアログ「コントローラーアプレット」は表示されません。このダイアログを閉じるか、新しいコントローラーを設定するように求めるプロンプトは表示されません。以前に切断されたコントローラーが再接続されると、ゲームは自動的に再開されます。", "ko_KR": "게임 플레이 중에 게임패드 연결이 끊어지면 외부 대화 상자 \"컨트롤러 애플릿\"이 나타나지 않습니다. 대화 상자를 닫거나 새 컨트롤러를 설정하라는 메시지가 표시되지 않습니다. 이전에 연결이 끊어진 컨트롤러가 다시 연결되면 게임이 자동으로 다시 시작됩니다.", - "no_NO": "", + "no_NO": "Den eksterne dialogboksen «Controller Applet» vises ikke hvis gamepaden kobles fra under spilling. Du blir ikke bedt om å lukke dialogboksen eller konfigurere en ny kontroller. Når den tidligere frakoblede kontrolleren er koblet til igjen, fortsetter spillet automatisk.", "pl_PL": "Zewnętrzny dialog \"Controller Applet\" nie pojawi się, jeśli gamepad zostanie odłączony podczas rozgrywki. Nie pojawi się monit o zamknięcie dialogu lub skonfigurowanie nowego kontrolera. Po ponownym podłączeniu poprzednio odłączonego kontrolera gra zostanie automatycznie wznowiona.", "pt_BR": "O diálogo externo \"Controller Applet\" não aparecerá se o gamepad for desconectado durante o jogo. Não haverá prompt para fechar o diálogo ou configurar um novo controle. Assim que o controle desconectado anteriormente for reconectado, o jogo será retomado automaticamente.", "ru_RU": "Внешний диалог \"Апплет контроллера\" не появится, если геймпад будет отключен во время игры. Не будет предложено закрыть диалог или настроить новый контроллер. После повторного подключения ранее отключенного контроллера игра автоматически возобновится.", @@ -15514,7 +15514,7 @@ "it_IT": "Apri un esploratore file per scegliere una o più cartelle dalle quali caricare DLC in massa", "ja_JP": "", "ko_KR": "파일 탐색기를 열어 DLC를 일괄 불러오기할 폴더를 하나 이상 선택", - "no_NO": "", + "no_NO": "Åpne en filutforsker for å velge en eller flere mapper å laste inn DLC fra", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -15538,7 +15538,7 @@ "it_IT": "Apri un esploratore file per scegliere una o più cartelle dalle quali caricare aggiornamenti in massa", "ja_JP": "", "ko_KR": "파일 탐색기를 열어 하나 이상의 폴더를 선택하여 대량으로 타이틀 업데이트 불러오기", - "no_NO": "", + "no_NO": "Åpne en filutforsker for å velge en eller flere mapper som du vil laste inn titteloppdateringer fra", "pl_PL": "", "pt_BR": "Abra o explorador de arquivos para selecionar uma ou mais pastas e carregar atualizações de jogo em massa.", "ru_RU": "", @@ -16810,7 +16810,7 @@ "it_IT": "Parziale", "ja_JP": "", "ko_KR": "일부", - "no_NO": "", + "no_NO": "Delvis", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -16834,7 +16834,7 @@ "it_IT": "Non Trimmato", "ja_JP": "", "ko_KR": "트리밍되지 않음", - "no_NO": "", + "no_NO": "Ikke trimmet", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -16858,7 +16858,7 @@ "it_IT": "Trimmato", "ja_JP": "", "ko_KR": "트리밍됨", - "no_NO": "", + "no_NO": "Trimmet", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -16882,7 +16882,7 @@ "it_IT": "(Fallito)", "ja_JP": "", "ko_KR": "(실패)", - "no_NO": "", + "no_NO": "(Mislyktes)", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -16906,7 +16906,7 @@ "it_IT": "Salva {0:n0} Mb", "ja_JP": "", "ko_KR": "{0:n0} Mb 저장", - "no_NO": "", + "no_NO": "Spare {0:n0} Mb", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -16930,7 +16930,7 @@ "it_IT": "Salva {0:n0} Mb", "ja_JP": "", "ko_KR": "{0:n0}Mb 저장됨", - "no_NO": "", + "no_NO": "Spart {0:n0} Mb", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -17098,7 +17098,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "캐비닛 대화 상자", - "no_NO": "", + "no_NO": "Dialogboks for kabinett", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -17122,7 +17122,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "Amiibo의 새 이름 입력하기", - "no_NO": "", + "no_NO": "Skriv inn Amiiboens nye navn", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -17146,7 +17146,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "지금 Amiibo를 스캔하세요.", - "no_NO": "", + "no_NO": "Vennligst skann Amiiboene dine nå.", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18226,7 +18226,7 @@ "it_IT": "Controlla e Trimma i file XCI ", "ja_JP": "", "ko_KR": "XCI 파일 확인 및 정리", - "no_NO": "", + "no_NO": "Kontroller og trim XCI-filen", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18250,7 +18250,7 @@ "it_IT": "Questa funzionalita controllerà prima lo spazio libero e poi trimmerà il file XCI per liberare dello spazio.", "ja_JP": "", "ko_KR": "이 기능은 먼저 충분한 공간을 확보한 다음 XCI 파일을 트리밍하여 디스크 공간을 절약합니다.", - "no_NO": "", + "no_NO": "Denne funksjonen kontrollerer først hvor mye plass som er ledig, og trimmer deretter XCI-filen for å spare diskplass.", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18274,7 +18274,7 @@ "it_IT": "Dimensioni Attuali File: {0:n} MB\nDimensioni Dati Gioco: {1:n} MB\nRisparimio Spazio Disco: {2:n} MB", "ja_JP": "", "ko_KR": "현재 파일 크기 : {0:n}MB\n게임 데이터 크기 : {1:n}MB\n디스크 공간 절약 : {2:n}MB", - "no_NO": "", + "no_NO": "Nåværende filstørrelse: 0:n MB\nSpilldatastørrelse: {1:n} MB\nDiskplassbesparelse: {2:n} MB", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18298,7 +18298,7 @@ "it_IT": "Il file XCI non deve essere trimmato. Controlla i log per ulteriori dettagli", "ja_JP": "", "ko_KR": "XCI 파일은 트리밍할 필요가 없습니다. 자세한 내용은 로그를 확인", - "no_NO": "", + "no_NO": "XCI-filen trenger ikke å trimmes. Sjekk loggene for mer informasjon", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18322,7 +18322,7 @@ "it_IT": "Il file XCI non può essere untrimmato. Controlla i log per ulteriori dettagli", "ja_JP": "", "ko_KR": "XCI 파일은 트리밍을 해제할 수 없습니다. 자세한 내용은 로그를 확인", - "no_NO": "", + "no_NO": "XCI-filen kan ikke trimmes. Sjekk loggene for mer informasjon", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18346,7 +18346,7 @@ "it_IT": "Il file XCI è in sola lettura e non può essere reso Scrivibile. Controlla i log per ulteriori dettagli", "ja_JP": "", "ko_KR": "XCI 파일은 읽기 전용이므로 쓰기 가능하게 만들 수 없습니다. 자세한 내용은 로그를 확인", - "no_NO": "", + "no_NO": "XCI-filen er skrivebeskyttet og kunne ikke gjøres skrivbar. Sjekk loggene for mer informasjon", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18370,7 +18370,7 @@ "it_IT": "Il file XCI ha cambiato dimensioni da quando è stato scansionato. Controlla che il file non stia venendo scritto da qualche altro programma e poi riprova.", "ja_JP": "", "ko_KR": "XCI 파일이 스캔된 후 크기가 변경되었습니다. 파일이 쓰여지고 있지 않은지 확인하고 다시 시도하세요.", - "no_NO": "", + "no_NO": "XCI File har endret størrelse siden den ble skannet. Kontroller at det ikke skrives til filen, og prøv på nytt.", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18394,7 +18394,7 @@ "it_IT": "Il file XCI ha dati nello spazio libero, non è sicuro effettuare il trimming", "ja_JP": "", "ko_KR": "XCI 파일에 여유 공간 영역에 데이터가 있으므로 트리밍하는 것이 안전하지 않음", - "no_NO": "", + "no_NO": "XCI-filen har data i ledig plass, og det er ikke trygt å trimme den", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18418,7 +18418,7 @@ "it_IT": "Il file XCI contiene dati invlidi. Controlla i log per ulteriori dettagli", "ja_JP": "", "ko_KR": "XCI 파일에 유효하지 않은 데이터가 포함되어 있습니다. 자세한 내용은 로그를 확인", - "no_NO": "", + "no_NO": "XCI-filen inneholder ugyldige data. Sjekk loggene for ytterligere detaljer", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18442,7 +18442,7 @@ "it_IT": "Il file XCI non può essere aperto per essere scritto. Controlla i log per ulteriori dettagli", "ja_JP": "", "ko_KR": "XCI 파일을 쓰기 위해 열 수 없습니다. 자세한 내용은 로그를 확인", - "no_NO": "", + "no_NO": "XCI-filen kunne ikke åpnes for skriving. Sjekk loggene for ytterligere detaljer", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18466,7 +18466,7 @@ "it_IT": "Trimming del file XCI fallito", "ja_JP": "", "ko_KR": "XCI 파일 트리밍에 실패", - "no_NO": "", + "no_NO": "Trimming av XCI-filen mislyktes", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18490,7 +18490,7 @@ "it_IT": "Operazione Cancellata", "ja_JP": "", "ko_KR": "작업이 취소됨", - "no_NO": "", + "no_NO": "Operasjonen ble avlyst", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18514,7 +18514,7 @@ "it_IT": "Nessuna operazione è stata effettuata", "ja_JP": "", "ko_KR": "작업이 수행되지 않음", - "no_NO": "", + "no_NO": "Ingen operasjon ble utført", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18658,7 +18658,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "XCI 파일 트리머", - "no_NO": "", + "no_NO": "XCI File Trimmer", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18682,7 +18682,7 @@ "it_IT": "{0} di {1} Titolo(i) Selezionati", "ja_JP": "", "ko_KR": "{1}개 타이틀 중 {0}개 선택됨", - "no_NO": "", + "no_NO": "{0} av {1} Valgte tittel(er)", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18706,7 +18706,7 @@ "it_IT": "{0} of {1} Titolo(i) Selezionati ({2} visualizzato)", "ja_JP": "", "ko_KR": "{1}개 타이틀 중 {0}개 선택됨({2}개 표시됨)", - "no_NO": "", + "no_NO": "{0} av {1} Tittel(er) valgt ({2} vises)", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18730,7 +18730,7 @@ "it_IT": "Trimming {0} Titolo(i)...", "ja_JP": "", "ko_KR": "{0}개의 타이틀을 트리밍 중...", - "no_NO": "", + "no_NO": "Trimming av {0} tittel(er)...", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18754,7 +18754,7 @@ "it_IT": "Untrimming {0} Titolo(i)...", "ja_JP": "", "ko_KR": "{0}개의 타이틀을 트리밍 해제 중...", - "no_NO": "", + "no_NO": "Untrimming {0} Tittel(er)...", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18778,7 +18778,7 @@ "it_IT": "Fallito", "ja_JP": "", "ko_KR": "실패", - "no_NO": "", + "no_NO": "Mislyktes", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18802,7 +18802,7 @@ "it_IT": "Potenziali Salvataggi", "ja_JP": "", "ko_KR": "잠재적 비용 절감", - "no_NO": "", + "no_NO": "Potensielle besparelser", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18826,7 +18826,7 @@ "it_IT": "Effettivi Salvataggi", "ja_JP": "", "ko_KR": "실제 비용 절감", - "no_NO": "", + "no_NO": "Faktiske besparelser", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18874,7 +18874,7 @@ "it_IT": "Seleziona Visualizzati", "ja_JP": "", "ko_KR": "표시됨 선택", - "no_NO": "", + "no_NO": "Velg vist", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18898,7 +18898,7 @@ "it_IT": "Deselziona Visualizzati", "ja_JP": "", "ko_KR": "표시됨 선택 취소", - "no_NO": "", + "no_NO": "Opphev valg av Vist", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18922,7 +18922,7 @@ "it_IT": "Titolo", "ja_JP": "", "ko_KR": "타이틀", - "no_NO": "", + "no_NO": "Tittel", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18946,7 +18946,7 @@ "it_IT": "Salvataggio Spazio", "ja_JP": "", "ko_KR": "공간 절약s", - "no_NO": "", + "no_NO": "Plassbesparelser", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -18994,7 +18994,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "언트림", - "no_NO": "", + "no_NO": "Utrim", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -19018,7 +19018,7 @@ "it_IT": "{0} aggiornamento/i aggiunto/i", "ja_JP": "", "ko_KR": "{0}개의 새 업데이트가 추가됨", - "no_NO": "", + "no_NO": "{0} ny(e) oppdatering(er) lagt til", "pl_PL": "", "pt_BR": "{0} nova(s) atualização(ões) adicionada(s)", "ru_RU": "", @@ -19042,7 +19042,7 @@ "it_IT": "Gli aggiornamenti inclusi non possono essere eliminati, ma solo disattivati", "ja_JP": "", "ko_KR": "번들 업데이트는 제거할 수 없으며, 비활성화만 가능합니다.", - "no_NO": "", + "no_NO": "Medfølgende oppdateringer kan ikke fjernes, bare deaktiveres.", "pl_PL": "", "pt_BR": "Atualizações incorporadas não podem ser removidas, apenas desativadas.", "ru_RU": "", @@ -19114,7 +19114,7 @@ "it_IT": "i DLC \"impacchettati\" non possono essere rimossi, ma solo disabilitati.", "ja_JP": "", "ko_KR": "번들 DLC는 제거할 수 없으며 비활성화만 가능합니다.", - "no_NO": "", + "no_NO": "Medfølgende DLC kan ikke fjernes, bare deaktiveres.", "pl_PL": "", "pt_BR": "DLCs incorporadas não podem ser removidas, apenas desativadas.", "ru_RU": "", @@ -19162,7 +19162,7 @@ "it_IT": "{0} nuovo/i contenuto/i scaricabile/i aggiunto/i", "ja_JP": "", "ko_KR": "{0}개의 새로운 내려받기 가능한 콘텐츠가 추가됨", - "no_NO": "", + "no_NO": "{0} nytt nedlastbart innhold lagt til", "pl_PL": "", "pt_BR": "{0} novo(s) conteúdo(s) para download adicionado(s)", "ru_RU": "", @@ -19186,7 +19186,7 @@ "it_IT": "{0} contenuto/i scaricabile/i aggiunto/i", "ja_JP": "", "ko_KR": "{0}개의 새로운 내려받기 가능한 콘텐츠가 추가됨", - "no_NO": "", + "no_NO": "{0} nytt nedlastbart innhold lagt til", "pl_PL": "", "pt_BR": "{0} novo(s) conteúdo(s) para download adicionado(s)", "ru_RU": "", @@ -19210,7 +19210,7 @@ "it_IT": "{0} contenuto/i scaricabile/i mancante/i rimosso/i", "ja_JP": "", "ko_KR": "{0}개의 내려받기 가능한 콘텐츠가 제거됨", - "no_NO": "", + "no_NO": "{0} manglende nedlastbart innhold fjernet", "pl_PL": "", "pt_BR": "{0} conteúdo(s) para download ausente(s) removido(s)", "ru_RU": "", @@ -19234,7 +19234,7 @@ "it_IT": "{0} aggiornamento/i aggiunto/i", "ja_JP": "", "ko_KR": "{0}개의 새 업데이트가 추가됨", - "no_NO": "", + "no_NO": "{0} ny(e) oppdatering(er) lagt til", "pl_PL": "", "pt_BR": "{0} nova(s) atualização(ões) adicionada(s)", "ru_RU": "", @@ -19258,7 +19258,7 @@ "it_IT": "{0} aggiornamento/i mancante/i rimosso/i", "ja_JP": "", "ko_KR": "누락된 업데이트 {0}개 삭제", - "no_NO": "", + "no_NO": "{0} manglende oppdatering(er) fjernet", "pl_PL": "", "pt_BR": "{0} atualização(ões) ausente(s) removida(s)", "ru_RU": "", @@ -19330,7 +19330,7 @@ "it_IT": "Continua", "ja_JP": "", "ko_KR": "계속", - "no_NO": "", + "no_NO": "Fortsett", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21010,7 +21010,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "P2P 네트워크 호스팅 비활성화(대기 시간이 늘어날 수 있음)", - "no_NO": "", + "no_NO": "Deaktiver P2P-nettverkshosting (kan øke ventetiden)", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21034,7 +21034,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "P2P 네트워크 호스팅을 비활성화하면 피어가 직접 연결하지 않고 마스터 서버를 통해 프록시합니다.", - "no_NO": "", + "no_NO": "Deaktiver P2P-nettverkshosting, så vil andre brukere gå via hovedserveren i stedet for å koble seg direkte til deg.", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21058,7 +21058,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "네트워크 암호 문구 :", - "no_NO": "", + "no_NO": "Nettverkspassord:", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21082,7 +21082,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "귀하는 귀하와 동일한 암호를 사용하는 호스팅 게임만 볼 수 있습니다.", - "no_NO": "", + "no_NO": "Du vil bare kunne se spill som er arrangert med samme passordfrase som deg.", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21106,7 +21106,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "Ryujinx-<8 hex chars> 형식으로 암호를 입력하세요. 귀하는 귀하와 동일한 암호를 사용하는 호스팅 게임만 볼 수 있습니다.", - "no_NO": "", + "no_NO": "Skriv inn en passordfrase i formatet Ryujinx-<8 heks tegn>. Du vil bare kunne se spill som er arrangert med samme passordfrase som deg.", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21130,7 +21130,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "(일반)", - "no_NO": "", + "no_NO": "(offentlig)", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21154,7 +21154,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "무작위 생성", - "no_NO": "", + "no_NO": "Generer tilfeldig", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21178,7 +21178,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "다른 플레이어와 공유할 수 있는 새로운 암호 문구를 생성합니다.", - "no_NO": "", + "no_NO": "Genererer en ny passordfrase, som kan deles med andre spillere.", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21202,7 +21202,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "지우기", - "no_NO": "", + "no_NO": "Slett", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21226,7 +21226,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "현재 암호를 지우고 공용 네트워크로 돌아갑니다.", - "no_NO": "", + "no_NO": "Sletter den gjeldende passordfrasen og går tilbake til det offentlige nettverket.", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21250,7 +21250,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "유효하지 않은 암호입니다! \"Ryujinx-<8 hex chars>\" 형식이어야 합니다.", - "no_NO": "", + "no_NO": "Ugyldig passordfrase! Må være i formatet \"Ryujinx-<8 hex tegn>\"", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21298,7 +21298,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "사용자 정의 주사율 활성화(실험적)", - "no_NO": "", + "no_NO": "Aktiver egendefinert oppdateringsfrekvens (eksperimentell)", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21346,7 +21346,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "무제한", - "no_NO": "", + "no_NO": "Ubegrenset", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21370,7 +21370,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "사용자 정의 주사율", - "no_NO": "", + "no_NO": "Egendefinert oppdateringsfrekvens", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21394,7 +21394,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "에뮬레이트된 수직 동기화. '스위치'는 스위치의 60Hz 주사율을 에뮬레이트합니다. '무한'은 무제한 주사율입니다.", - "no_NO": "", + "no_NO": "Emulert vertikal synkronisering. «Switch» emulerer Switchs oppdateringsfrekvens på 60 Hz. «Ubegrenset» er en ubegrenset oppdateringsfrekvens.", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21418,7 +21418,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "에뮬레이트된 수직 동기화. '스위치'는 스위치의 60Hz 주사율을 에뮬레이트합니다. '무한'은 무제한 주사율입니다. '사용자 지정'은 지정된 사용자 지정 주사율을 에뮬레이트합니다.", - "no_NO": "", + "no_NO": "Emulert vertikal synkronisering. «Switch» emulerer Switchs oppdateringsfrekvens på 60 Hz. «Ubegrenset» er en ubegrenset oppdateringsfrekvens. «Egendefinert» emulerer den angitte egendefinerte oppdateringsfrekvensen.", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21442,7 +21442,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "사용자가 에뮬레이트된 화면 주사율을 지정할 수 있습니다. 일부 타이틀에서는 게임플레이 로직 속도가 빨라지거나 느려질 수 있습니다. 다른 타이틀에서는 주사율의 배수로 FPS를 제한하거나 예측할 수 없는 동작으로 이어질 수 있습니다. 이는 실험적 기능으로 게임 플레이에 어떤 영향을 미칠지 보장할 수 없습니다. \n\n모르면 끔으로 두세요.", - "no_NO": "", + "no_NO": "Gjør det mulig for brukeren å angi en emulert oppdateringsfrekvens. I noen titler kan dette øke eller senke hastigheten på spillogikken. I andre titler kan det gjøre det mulig å begrense FPS til et multiplum av oppdateringsfrekvensen, eller føre til uforutsigbar oppførsel. Dette er en eksperimentell funksjon, og det gis ingen garantier for hvordan spillingen påvirkes. \n\nLa AV stå hvis du er usikker.", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21466,7 +21466,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "사용자 정의 주사율 목표 값입니다.", - "no_NO": "", + "no_NO": "Den egendefinerte målverdien for oppdateringsfrekvens.", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21490,7 +21490,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "일반 스위치 주사율의 백분율로 나타낸 사용자 지정 주사율입니다.", - "no_NO": "", + "no_NO": "Den egendefinerte oppdateringsfrekvensen, i prosent av den normale oppdateringsfrekvensen for Switch-konsollen.", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21514,7 +21514,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "사용자 정의 주사율 % :", - "no_NO": "", + "no_NO": "Egendefinert oppdateringsfrekvens %:", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21538,7 +21538,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "사용자 정의 주사율 값 :", - "no_NO": "", + "no_NO": "Egendefinert verdi for oppdateringsfrekvens:", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21562,7 +21562,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "간격", - "no_NO": "", + "no_NO": "Intervall", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21586,7 +21586,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "수직 동기화 모드 전환 :", - "no_NO": "", + "no_NO": "Veksle mellom VSync-modus:", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21610,7 +21610,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "사용자 정의 주사율 증가", - "no_NO": "", + "no_NO": "Øk den egendefinerte oppdateringsfrekvensen", "pl_PL": "", "pt_BR": "", "ru_RU": "", @@ -21634,7 +21634,7 @@ "it_IT": "", "ja_JP": "", "ko_KR": "사용자 정의 주사율 감소", - "no_NO": "", + "no_NO": "Lavere tilpasset oppdateringsfrekvens", "pl_PL": "", "pt_BR": "", "ru_RU": "", From 852823104f3af3d67484377b5d8d8ba370687ccb Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 24 Dec 2024 00:55:16 -0600 Subject: [PATCH 65/79] EXPERIMENTAL: Metal backend (#441) This is not a continuation of the Metal backend; this is simply bringing the branch up to date and merging it as-is behind an experiment. --------- Co-authored-by: Isaac Marovitz Co-authored-by: Samuliak Co-authored-by: SamoZ256 <96914946+SamoZ256@users.noreply.github.com> Co-authored-by: Isaac Marovitz <42140194+IsaacMarovitz@users.noreply.github.com> Co-authored-by: riperiperi Co-authored-by: Gabriel A --- Directory.Packages.props | 1 + Ryujinx.sln | 8 + .../Configuration/GraphicsBackend.cs | 2 + src/Ryujinx.Graphics.GAL/ComputeSize.cs | 18 + src/Ryujinx.Graphics.GAL/Format.cs | 78 + src/Ryujinx.Graphics.GAL/ShaderInfo.cs | 17 +- .../Threed/ComputeDraw/VtgAsComputeContext.cs | 19 +- .../Threed/ComputeDraw/VtgAsComputeState.cs | 18 - .../Shader/DiskCache/DiskCacheHostStorage.cs | 13 +- .../DiskCache/ParallelDiskCacheLoader.cs | 7 +- .../Shader/GpuAccessor.cs | 8 +- .../Shader/GpuAccessorBase.cs | 8 +- .../Shader/GpuChannelComputeState.cs | 11 + .../Shader/ShaderCache.cs | 21 +- .../Shader/ShaderInfoBuilder.cs | 51 +- src/Ryujinx.Graphics.Metal/Auto.cs | 146 ++ .../BackgroundResources.cs | 107 + src/Ryujinx.Graphics.Metal/BitMap.cs | 157 ++ src/Ryujinx.Graphics.Metal/BufferHolder.cs | 385 ++++ src/Ryujinx.Graphics.Metal/BufferManager.cs | 237 +++ .../BufferUsageBitmap.cs | 85 + src/Ryujinx.Graphics.Metal/CacheByRange.cs | 294 +++ .../CommandBufferEncoder.cs | 170 ++ .../CommandBufferPool.cs | 289 +++ .../CommandBufferScoped.cs | 43 + src/Ryujinx.Graphics.Metal/Constants.cs | 41 + src/Ryujinx.Graphics.Metal/CounterEvent.cs | 22 + .../DepthStencilCache.cs | 68 + .../DisposableBuffer.cs | 26 + .../DisposableSampler.cs | 22 + .../Effects/IPostProcessingEffect.cs | 10 + .../Effects/IScalingFilter.cs | 18 + .../EncoderResources.cs | 63 + src/Ryujinx.Graphics.Metal/EncoderState.cs | 206 ++ .../EncoderStateManager.cs | 1788 +++++++++++++++++ src/Ryujinx.Graphics.Metal/EnumConversion.cs | 293 +++ src/Ryujinx.Graphics.Metal/FenceHolder.cs | 77 + src/Ryujinx.Graphics.Metal/FormatConverter.cs | 49 + src/Ryujinx.Graphics.Metal/FormatTable.cs | 196 ++ src/Ryujinx.Graphics.Metal/HardwareInfo.cs | 82 + src/Ryujinx.Graphics.Metal/HashTableSlim.cs | 143 ++ src/Ryujinx.Graphics.Metal/HelperShader.cs | 868 ++++++++ src/Ryujinx.Graphics.Metal/IdList.cs | 121 ++ src/Ryujinx.Graphics.Metal/ImageArray.cs | 74 + .../IndexBufferPattern.cs | 118 ++ .../IndexBufferState.cs | 103 + src/Ryujinx.Graphics.Metal/MetalRenderer.cs | 309 +++ .../MultiFenceHolder.cs | 262 +++ .../PersistentFlushBuffer.cs | 99 + src/Ryujinx.Graphics.Metal/Pipeline.cs | 877 ++++++++ src/Ryujinx.Graphics.Metal/Program.cs | 286 +++ .../ResourceBindingSegment.cs | 22 + .../ResourceLayoutBuilder.cs | 59 + .../Ryujinx.Graphics.Metal.csproj | 30 + src/Ryujinx.Graphics.Metal/SamplerHolder.cs | 90 + src/Ryujinx.Graphics.Metal/Shaders/Blit.metal | 43 + .../Shaders/BlitMs.metal | 45 + .../Shaders/ChangeBufferStride.metal | 72 + .../Shaders/ColorClear.metal | 38 + .../Shaders/ConvertD32S8ToD24S8.metal | 66 + .../Shaders/ConvertIndexBuffer.metal | 59 + .../Shaders/DepthBlit.metal | 27 + .../Shaders/DepthBlitMs.metal | 29 + .../Shaders/DepthStencilClear.metal | 42 + .../Shaders/StencilBlit.metal | 27 + .../Shaders/StencilBlitMs.metal | 29 + src/Ryujinx.Graphics.Metal/StagingBuffer.cs | 288 +++ .../State/DepthStencilUid.cs | 110 + .../State/PipelineState.cs | 341 ++++ .../State/PipelineUid.cs | 208 ++ src/Ryujinx.Graphics.Metal/StateCache.cs | 42 + src/Ryujinx.Graphics.Metal/StringHelper.cs | 30 + src/Ryujinx.Graphics.Metal/SyncManager.cs | 214 ++ src/Ryujinx.Graphics.Metal/Texture.cs | 654 ++++++ src/Ryujinx.Graphics.Metal/TextureArray.cs | 93 + src/Ryujinx.Graphics.Metal/TextureBase.cs | 67 + src/Ryujinx.Graphics.Metal/TextureBuffer.cs | 132 ++ src/Ryujinx.Graphics.Metal/TextureCopy.cs | 265 +++ .../VertexBufferState.cs | 60 + src/Ryujinx.Graphics.Metal/Window.cs | 231 +++ .../CodeGen/Msl/CodeGenContext.cs | 108 + .../CodeGen/Msl/Declarations.cs | 578 ++++++ .../CodeGen/Msl/Defaults.cs | 34 + .../CodeGen/Msl/HelperFunctions/FindLSB.metal | 5 + .../Msl/HelperFunctions/FindMSBS32.metal | 5 + .../Msl/HelperFunctions/FindMSBU32.metal | 6 + .../HelperFunctions/HelperFunctionNames.cs | 10 + .../CodeGen/Msl/HelperFunctions/Precise.metal | 14 + .../Msl/HelperFunctions/SwizzleAdd.metal | 7 + .../CodeGen/Msl/Instructions/InstGen.cs | 185 ++ .../CodeGen/Msl/Instructions/InstGenBallot.cs | 30 + .../Msl/Instructions/InstGenBarrier.cs | 15 + .../CodeGen/Msl/Instructions/InstGenCall.cs | 60 + .../CodeGen/Msl/Instructions/InstGenHelper.cs | 222 ++ .../CodeGen/Msl/Instructions/InstGenMemory.cs | 672 +++++++ .../CodeGen/Msl/Instructions/InstGenVector.cs | 32 + .../CodeGen/Msl/Instructions/InstInfo.cs | 18 + .../CodeGen/Msl/Instructions/InstType.cs | 35 + .../CodeGen/Msl/Instructions/IoMap.cs | 83 + .../CodeGen/Msl/MslGenerator.cs | 286 +++ .../CodeGen/Msl/NumberFormatter.cs | 94 + .../CodeGen/Msl/OperandManager.cs | 176 ++ .../CodeGen/Msl/TypeConversion.cs | 93 + .../Ryujinx.Graphics.Shader.csproj | 7 + src/Ryujinx.Graphics.Shader/SamplerType.cs | 46 + .../StructuredIr/HelperFunctionsMask.cs | 7 + .../StructuredIr/StructuredProgram.cs | 17 +- .../StructuredIr/StructuredProgramContext.cs | 3 +- .../StructuredIr/StructuredProgramInfo.cs | 7 +- .../Translation/FeatureFlags.cs | 1 + .../Translation/ResourceManager.cs | 103 + .../Translation/TargetApi.cs | 1 + .../Translation/TargetLanguage.cs | 2 +- .../Transforms/ForcePreciseEnable.cs | 2 + .../Translation/TranslatorContext.cs | 34 +- .../Metal/MetalWindow.cs | 47 + src/Ryujinx.Headless.SDL2/Program.cs | 18 +- .../Ryujinx.Headless.SDL2.csproj | 1 + src/Ryujinx.ShaderTools/Program.cs | 2 +- .../Configuration/ConfigurationState.cs | 19 +- src/Ryujinx/AppHost.cs | 43 +- src/Ryujinx/Assets/locales.json | 48 + src/Ryujinx/Program.cs | 1 + src/Ryujinx/Ryujinx.csproj | 1 + .../UI/Renderer/EmbeddedWindowMetal.cs | 20 + src/Ryujinx/UI/Renderer/RendererHost.axaml.cs | 62 +- .../UI/ViewModels/MainWindowViewModel.cs | 2 +- .../UI/ViewModels/SettingsViewModel.cs | 7 +- .../UI/Views/Settings/SettingsCPUView.axaml | 2 +- .../Views/Settings/SettingsGraphicsView.axaml | 16 +- .../UI/Windows/SettingsWindow.axaml.cs | 18 +- 131 files changed, 14992 insertions(+), 140 deletions(-) create mode 100644 src/Ryujinx.Graphics.GAL/ComputeSize.cs create mode 100644 src/Ryujinx.Graphics.Metal/Auto.cs create mode 100644 src/Ryujinx.Graphics.Metal/BackgroundResources.cs create mode 100644 src/Ryujinx.Graphics.Metal/BitMap.cs create mode 100644 src/Ryujinx.Graphics.Metal/BufferHolder.cs create mode 100644 src/Ryujinx.Graphics.Metal/BufferManager.cs create mode 100644 src/Ryujinx.Graphics.Metal/BufferUsageBitmap.cs create mode 100644 src/Ryujinx.Graphics.Metal/CacheByRange.cs create mode 100644 src/Ryujinx.Graphics.Metal/CommandBufferEncoder.cs create mode 100644 src/Ryujinx.Graphics.Metal/CommandBufferPool.cs create mode 100644 src/Ryujinx.Graphics.Metal/CommandBufferScoped.cs create mode 100644 src/Ryujinx.Graphics.Metal/Constants.cs create mode 100644 src/Ryujinx.Graphics.Metal/CounterEvent.cs create mode 100644 src/Ryujinx.Graphics.Metal/DepthStencilCache.cs create mode 100644 src/Ryujinx.Graphics.Metal/DisposableBuffer.cs create mode 100644 src/Ryujinx.Graphics.Metal/DisposableSampler.cs create mode 100644 src/Ryujinx.Graphics.Metal/Effects/IPostProcessingEffect.cs create mode 100644 src/Ryujinx.Graphics.Metal/Effects/IScalingFilter.cs create mode 100644 src/Ryujinx.Graphics.Metal/EncoderResources.cs create mode 100644 src/Ryujinx.Graphics.Metal/EncoderState.cs create mode 100644 src/Ryujinx.Graphics.Metal/EncoderStateManager.cs create mode 100644 src/Ryujinx.Graphics.Metal/EnumConversion.cs create mode 100644 src/Ryujinx.Graphics.Metal/FenceHolder.cs create mode 100644 src/Ryujinx.Graphics.Metal/FormatConverter.cs create mode 100644 src/Ryujinx.Graphics.Metal/FormatTable.cs create mode 100644 src/Ryujinx.Graphics.Metal/HardwareInfo.cs create mode 100644 src/Ryujinx.Graphics.Metal/HashTableSlim.cs create mode 100644 src/Ryujinx.Graphics.Metal/HelperShader.cs create mode 100644 src/Ryujinx.Graphics.Metal/IdList.cs create mode 100644 src/Ryujinx.Graphics.Metal/ImageArray.cs create mode 100644 src/Ryujinx.Graphics.Metal/IndexBufferPattern.cs create mode 100644 src/Ryujinx.Graphics.Metal/IndexBufferState.cs create mode 100644 src/Ryujinx.Graphics.Metal/MetalRenderer.cs create mode 100644 src/Ryujinx.Graphics.Metal/MultiFenceHolder.cs create mode 100644 src/Ryujinx.Graphics.Metal/PersistentFlushBuffer.cs create mode 100644 src/Ryujinx.Graphics.Metal/Pipeline.cs create mode 100644 src/Ryujinx.Graphics.Metal/Program.cs create mode 100644 src/Ryujinx.Graphics.Metal/ResourceBindingSegment.cs create mode 100644 src/Ryujinx.Graphics.Metal/ResourceLayoutBuilder.cs create mode 100644 src/Ryujinx.Graphics.Metal/Ryujinx.Graphics.Metal.csproj create mode 100644 src/Ryujinx.Graphics.Metal/SamplerHolder.cs create mode 100644 src/Ryujinx.Graphics.Metal/Shaders/Blit.metal create mode 100644 src/Ryujinx.Graphics.Metal/Shaders/BlitMs.metal create mode 100644 src/Ryujinx.Graphics.Metal/Shaders/ChangeBufferStride.metal create mode 100644 src/Ryujinx.Graphics.Metal/Shaders/ColorClear.metal create mode 100644 src/Ryujinx.Graphics.Metal/Shaders/ConvertD32S8ToD24S8.metal create mode 100644 src/Ryujinx.Graphics.Metal/Shaders/ConvertIndexBuffer.metal create mode 100644 src/Ryujinx.Graphics.Metal/Shaders/DepthBlit.metal create mode 100644 src/Ryujinx.Graphics.Metal/Shaders/DepthBlitMs.metal create mode 100644 src/Ryujinx.Graphics.Metal/Shaders/DepthStencilClear.metal create mode 100644 src/Ryujinx.Graphics.Metal/Shaders/StencilBlit.metal create mode 100644 src/Ryujinx.Graphics.Metal/Shaders/StencilBlitMs.metal create mode 100644 src/Ryujinx.Graphics.Metal/StagingBuffer.cs create mode 100644 src/Ryujinx.Graphics.Metal/State/DepthStencilUid.cs create mode 100644 src/Ryujinx.Graphics.Metal/State/PipelineState.cs create mode 100644 src/Ryujinx.Graphics.Metal/State/PipelineUid.cs create mode 100644 src/Ryujinx.Graphics.Metal/StateCache.cs create mode 100644 src/Ryujinx.Graphics.Metal/StringHelper.cs create mode 100644 src/Ryujinx.Graphics.Metal/SyncManager.cs create mode 100644 src/Ryujinx.Graphics.Metal/Texture.cs create mode 100644 src/Ryujinx.Graphics.Metal/TextureArray.cs create mode 100644 src/Ryujinx.Graphics.Metal/TextureBase.cs create mode 100644 src/Ryujinx.Graphics.Metal/TextureBuffer.cs create mode 100644 src/Ryujinx.Graphics.Metal/TextureCopy.cs create mode 100644 src/Ryujinx.Graphics.Metal/VertexBufferState.cs create mode 100644 src/Ryujinx.Graphics.Metal/Window.cs create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/CodeGenContext.cs create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/Declarations.cs create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/Defaults.cs create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/FindLSB.metal create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/FindMSBS32.metal create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/FindMSBU32.metal create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/HelperFunctionNames.cs create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/Precise.metal create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/SwizzleAdd.metal create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGen.cs create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenBallot.cs create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenBarrier.cs create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenCall.cs create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenHelper.cs create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenMemory.cs create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenVector.cs create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstInfo.cs create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstType.cs create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/IoMap.cs create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/MslGenerator.cs create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/NumberFormatter.cs create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/OperandManager.cs create mode 100644 src/Ryujinx.Graphics.Shader/CodeGen/Msl/TypeConversion.cs create mode 100644 src/Ryujinx.Headless.SDL2/Metal/MetalWindow.cs create mode 100644 src/Ryujinx/UI/Renderer/EmbeddedWindowMetal.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 34655164e..07fc8cc28 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -44,6 +44,7 @@ + diff --git a/Ryujinx.sln b/Ryujinx.sln index c3cb5a2b0..71d5f6dd9 100644 --- a/Ryujinx.sln +++ b/Ryujinx.sln @@ -80,6 +80,10 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.Horizon.Kernel.Generators", "src\Ryujinx.Horizon.Kernel.Generators\Ryujinx.Horizon.Kernel.Generators.csproj", "{7F55A45D-4E1D-4A36-ADD3-87F29A285AA2}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ryujinx.HLE.Generators", "src\Ryujinx.HLE.Generators\Ryujinx.HLE.Generators.csproj", "{B575BCDE-2FD8-4A5D-8756-31CDD7FE81F0}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Ryujinx.Graphics.Metal", "src\Ryujinx.Graphics.Metal\Ryujinx.Graphics.Metal.csproj", "{C08931FA-1191-417A-864F-3882D93E683B}" + ProjectSection(ProjectDependencies) = postProject + {A602AE97-91A5-4608-8DF1-EBF4ED7A0B9E} = {A602AE97-91A5-4608-8DF1-EBF4ED7A0B9E} + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{36F870C1-3E5F-485F-B426-F0645AF78751}" ProjectSection(SolutionItems) = preProject @@ -257,6 +261,10 @@ Global {4A89A234-4F19-497D-A576-DDE8CDFC5B22}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4A89A234-4F19-497D-A576-DDE8CDFC5B22}.Debug|Any CPU.Build.0 = Debug|Any CPU {4A89A234-4F19-497D-A576-DDE8CDFC5B22}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C08931FA-1191-417A-864F-3882D93E683B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C08931FA-1191-417A-864F-3882D93E683B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C08931FA-1191-417A-864F-3882D93E683B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C08931FA-1191-417A-864F-3882D93E683B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Ryujinx.Common/Configuration/GraphicsBackend.cs b/src/Ryujinx.Common/Configuration/GraphicsBackend.cs index e3b4f91b0..9d399d560 100644 --- a/src/Ryujinx.Common/Configuration/GraphicsBackend.cs +++ b/src/Ryujinx.Common/Configuration/GraphicsBackend.cs @@ -6,7 +6,9 @@ namespace Ryujinx.Common.Configuration [JsonConverter(typeof(TypedStringEnumConverter))] public enum GraphicsBackend { + Auto, Vulkan, OpenGl, + Metal } } diff --git a/src/Ryujinx.Graphics.GAL/ComputeSize.cs b/src/Ryujinx.Graphics.GAL/ComputeSize.cs new file mode 100644 index 000000000..ce9c2531c --- /dev/null +++ b/src/Ryujinx.Graphics.GAL/ComputeSize.cs @@ -0,0 +1,18 @@ +namespace Ryujinx.Graphics.GAL +{ + public readonly struct ComputeSize + { + public readonly static ComputeSize VtgAsCompute = new ComputeSize(32, 32, 1); + + public readonly int X; + public readonly int Y; + public readonly int Z; + + public ComputeSize(int x, int y, int z) + { + X = x; + Y = y; + Z = z; + } + } +} diff --git a/src/Ryujinx.Graphics.GAL/Format.cs b/src/Ryujinx.Graphics.GAL/Format.cs index 17c42d2d4..b1eb68f72 100644 --- a/src/Ryujinx.Graphics.GAL/Format.cs +++ b/src/Ryujinx.Graphics.GAL/Format.cs @@ -339,6 +339,84 @@ namespace Ryujinx.Graphics.GAL return 1; } + /// + /// Get bytes per element for this format. + /// + /// Texture format + /// Byte size for an element of this format (pixel, vertex attribute, etc) + public static int GetBytesPerElement(this Format format) + { + int scalarSize = format.GetScalarSize(); + + switch (format) + { + case Format.R8G8Unorm: + case Format.R8G8Snorm: + case Format.R8G8Uint: + case Format.R8G8Sint: + case Format.R8G8Uscaled: + case Format.R8G8Sscaled: + case Format.R16G16Float: + case Format.R16G16Unorm: + case Format.R16G16Snorm: + case Format.R16G16Uint: + case Format.R16G16Sint: + case Format.R16G16Uscaled: + case Format.R16G16Sscaled: + case Format.R32G32Float: + case Format.R32G32Uint: + case Format.R32G32Sint: + case Format.R32G32Uscaled: + case Format.R32G32Sscaled: + return 2 * scalarSize; + + case Format.R8G8B8Unorm: + case Format.R8G8B8Snorm: + case Format.R8G8B8Uint: + case Format.R8G8B8Sint: + case Format.R8G8B8Uscaled: + case Format.R8G8B8Sscaled: + case Format.R16G16B16Float: + case Format.R16G16B16Unorm: + case Format.R16G16B16Snorm: + case Format.R16G16B16Uint: + case Format.R16G16B16Sint: + case Format.R16G16B16Uscaled: + case Format.R16G16B16Sscaled: + case Format.R32G32B32Float: + case Format.R32G32B32Uint: + case Format.R32G32B32Sint: + case Format.R32G32B32Uscaled: + case Format.R32G32B32Sscaled: + return 3 * scalarSize; + + case Format.R8G8B8A8Unorm: + case Format.R8G8B8A8Snorm: + case Format.R8G8B8A8Uint: + case Format.R8G8B8A8Sint: + case Format.R8G8B8A8Srgb: + case Format.R8G8B8A8Uscaled: + case Format.R8G8B8A8Sscaled: + case Format.B8G8R8A8Unorm: + case Format.B8G8R8A8Srgb: + case Format.R16G16B16A16Float: + case Format.R16G16B16A16Unorm: + case Format.R16G16B16A16Snorm: + case Format.R16G16B16A16Uint: + case Format.R16G16B16A16Sint: + case Format.R16G16B16A16Uscaled: + case Format.R16G16B16A16Sscaled: + case Format.R32G32B32A32Float: + case Format.R32G32B32A32Uint: + case Format.R32G32B32A32Sint: + case Format.R32G32B32A32Uscaled: + case Format.R32G32B32A32Sscaled: + return 4 * scalarSize; + } + + return scalarSize; + } + /// /// Checks if the texture format is a depth or depth-stencil format. /// diff --git a/src/Ryujinx.Graphics.GAL/ShaderInfo.cs b/src/Ryujinx.Graphics.GAL/ShaderInfo.cs index 2fd3227dc..c7965a03d 100644 --- a/src/Ryujinx.Graphics.GAL/ShaderInfo.cs +++ b/src/Ryujinx.Graphics.GAL/ShaderInfo.cs @@ -4,23 +4,22 @@ namespace Ryujinx.Graphics.GAL { public int FragmentOutputMap { get; } public ResourceLayout ResourceLayout { get; } + public ComputeSize ComputeLocalSize { get; } public ProgramPipelineState? State { get; } public bool FromCache { get; set; } - public ShaderInfo(int fragmentOutputMap, ResourceLayout resourceLayout, ProgramPipelineState state, bool fromCache = false) + public ShaderInfo( + int fragmentOutputMap, + ResourceLayout resourceLayout, + ComputeSize computeLocalSize, + ProgramPipelineState? state, + bool fromCache = false) { FragmentOutputMap = fragmentOutputMap; ResourceLayout = resourceLayout; + ComputeLocalSize = computeLocalSize; State = state; FromCache = fromCache; } - - public ShaderInfo(int fragmentOutputMap, ResourceLayout resourceLayout, bool fromCache = false) - { - FragmentOutputMap = fragmentOutputMap; - ResourceLayout = resourceLayout; - State = null; - FromCache = fromCache; - } } } diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs index 6de50fb2e..34f2cfcad 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeContext.cs @@ -11,8 +11,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw /// class VtgAsComputeContext : IDisposable { - private const int DummyBufferSize = 16; - private readonly GpuContext _context; /// @@ -48,7 +46,7 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw 1, 1, 1, - 1, + format.GetBytesPerElement(), format, DepthStencilMode.Depth, Target.TextureBuffer, @@ -521,21 +519,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw return new BufferRange(_geometryIndexDataBuffer.Handle, offset, size, write); } - /// - /// Gets the range for a dummy 16 bytes buffer, filled with zeros. - /// - /// Dummy buffer range - public BufferRange GetDummyBufferRange() - { - if (_dummyBuffer == BufferHandle.Null) - { - _dummyBuffer = _context.Renderer.CreateBuffer(DummyBufferSize, BufferAccess.DeviceMemory); - _context.Renderer.Pipeline.ClearBuffer(_dummyBuffer, 0, DummyBufferSize, 0); - } - - return new BufferRange(_dummyBuffer, 0, DummyBufferSize); - } - /// /// Gets the range for a sequential index buffer, with ever incrementing index values. /// diff --git a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs index 73682866b..2de324392 100644 --- a/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs +++ b/src/Ryujinx.Graphics.Gpu/Engine/Threed/ComputeDraw/VtgAsComputeState.cs @@ -147,7 +147,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw { _vacContext.VertexInfoBufferUpdater.SetVertexStride(index, 0, componentsCount); _vacContext.VertexInfoBufferUpdater.SetVertexOffset(index, 0, 0); - SetDummyBufferTexture(_vertexAsCompute.Reservations, index, format); continue; } @@ -163,15 +162,12 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw { _vacContext.VertexInfoBufferUpdater.SetVertexStride(index, 0, componentsCount); _vacContext.VertexInfoBufferUpdater.SetVertexOffset(index, 0, 0); - SetDummyBufferTexture(_vertexAsCompute.Reservations, index, format); continue; } int vbStride = vertexBuffer.UnpackStride(); ulong vbSize = GetVertexBufferSize(address, endAddress.Pack(), vbStride, _indexed, instanced, _firstVertex, _count); - ulong oldVbSize = vbSize; - ulong attributeOffset = (ulong)vertexAttrib.UnpackOffset(); int componentSize = format.GetScalarSize(); @@ -345,20 +341,6 @@ namespace Ryujinx.Graphics.Gpu.Engine.Threed.ComputeDraw return maxOutputVertices / verticesPerPrimitive; } - /// - /// Binds a dummy buffer as vertex buffer into a buffer texture. - /// - /// Shader resource binding reservations - /// Buffer texture index - /// Buffer texture format - private readonly void SetDummyBufferTexture(ResourceReservations reservations, int index, Format format) - { - ITexture bufferTexture = _vacContext.EnsureBufferTexture(index + 2, format); - bufferTexture.SetStorage(_vacContext.GetDummyBufferRange()); - - _context.Renderer.Pipeline.SetTextureAndSampler(ShaderStage.Compute, reservations.GetVertexBufferTextureBinding(index), bufferTexture, null); - } - /// /// Binds a vertex buffer into a buffer texture. /// diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs index c36fc0ada..b6b811389 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/DiskCacheHostStorage.cs @@ -324,6 +324,11 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache bool loadHostCache = header.CodeGenVersion == CodeGenVersion; + if (context.Capabilities.Api == TargetApi.Metal) + { + loadHostCache = false; + } + int programIndex = 0; DataEntry entry = new(); @@ -392,7 +397,8 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache context, shaders, specState.PipelineState, - specState.TransformFeedbackDescriptors != null); + specState.TransformFeedbackDescriptors != null, + specState.ComputeState.GetLocalSize()); IProgram hostProgram; @@ -629,7 +635,10 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache return; } - WriteHostCode(context, hostCode, program.Shaders, streams, timestamp); + if (context.Capabilities.Api != TargetApi.Metal) + { + WriteHostCode(context, hostCode, program.Shaders, streams, timestamp); + } } /// diff --git a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs index 20f96462e..74922d1e3 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/DiskCache/ParallelDiskCacheLoader.cs @@ -490,7 +490,12 @@ namespace Ryujinx.Graphics.Gpu.Shader.DiskCache { ShaderSource[] shaderSources = new ShaderSource[compilation.TranslatedStages.Length]; - ShaderInfoBuilder shaderInfoBuilder = new(_context, compilation.SpecializationState.TransformFeedbackDescriptors != null); + ref GpuChannelComputeState computeState = ref compilation.SpecializationState.ComputeState; + + ShaderInfoBuilder shaderInfoBuilder = new( + _context, + compilation.SpecializationState.TransformFeedbackDescriptors != null, + computeLocalSize: computeState.GetLocalSize()); for (int index = 0; index < compilation.TranslatedStages.Length; index++) { diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs index 1be75f242..4e9134291 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessor.cs @@ -16,7 +16,7 @@ namespace Ryujinx.Graphics.Gpu.Shader private readonly GpuAccessorState _state; private readonly int _stageIndex; private readonly bool _compute; - private readonly bool _isVulkan; + private readonly bool _isOpenGL; private readonly bool _hasGeometryShader; private readonly bool _supportsQuads; @@ -38,7 +38,7 @@ namespace Ryujinx.Graphics.Gpu.Shader _channel = channel; _state = state; _stageIndex = stageIndex; - _isVulkan = context.Capabilities.Api == TargetApi.Vulkan; + _isOpenGL = context.Capabilities.Api == TargetApi.OpenGL; _hasGeometryShader = hasGeometryShader; _supportsQuads = context.Capabilities.SupportsQuads; @@ -116,10 +116,10 @@ namespace Ryujinx.Graphics.Gpu.Shader public GpuGraphicsState QueryGraphicsState() { return _state.GraphicsState.CreateShaderGraphicsState( - !_isVulkan, + _isOpenGL, _supportsQuads, _hasGeometryShader, - _isVulkan || _state.GraphicsState.YNegateEnabled); + !_isOpenGL || _state.GraphicsState.YNegateEnabled); } /// diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs index d89eebabf..701ff764a 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuAccessorBase.cs @@ -55,7 +55,7 @@ namespace Ryujinx.Graphics.Gpu.Shader { int binding; - if (_context.Capabilities.Api == TargetApi.Vulkan) + if (_context.Capabilities.Api != TargetApi.OpenGL) { binding = GetBindingFromIndex(index, _context.Capabilities.MaximumUniformBuffersPerStage, "Uniform buffer"); } @@ -71,7 +71,7 @@ namespace Ryujinx.Graphics.Gpu.Shader { int binding; - if (_context.Capabilities.Api == TargetApi.Vulkan) + if (_context.Capabilities.Api != TargetApi.OpenGL) { if (count == 1) { @@ -103,7 +103,7 @@ namespace Ryujinx.Graphics.Gpu.Shader { int binding; - if (_context.Capabilities.Api == TargetApi.Vulkan) + if (_context.Capabilities.Api != TargetApi.OpenGL) { binding = GetBindingFromIndex(index, _context.Capabilities.MaximumStorageBuffersPerStage, "Storage buffer"); } @@ -119,7 +119,7 @@ namespace Ryujinx.Graphics.Gpu.Shader { int binding; - if (_context.Capabilities.Api == TargetApi.Vulkan) + if (_context.Capabilities.Api != TargetApi.OpenGL) { if (count == 1) { diff --git a/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelComputeState.cs b/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelComputeState.cs index d8cdbc348..720f7e796 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelComputeState.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/GpuChannelComputeState.cs @@ -1,3 +1,5 @@ +using Ryujinx.Graphics.GAL; + namespace Ryujinx.Graphics.Gpu.Shader { /// @@ -61,5 +63,14 @@ namespace Ryujinx.Graphics.Gpu.Shader SharedMemorySize = sharedMemorySize; HasUnalignedStorageBuffer = hasUnalignedStorageBuffer; } + + /// + /// Gets the local group size of the shader in a GAL compatible struct. + /// + /// Local group size + public ComputeSize GetLocalSize() + { + return new ComputeSize(LocalSizeX, LocalSizeY, LocalSizeZ); + } } } diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs index 4fc66c4c0..4473e8cb3 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs @@ -224,7 +224,10 @@ namespace Ryujinx.Graphics.Gpu.Shader TranslatedShader translatedShader = TranslateShader(_dumper, channel, translatorContext, cachedGuestCode, asCompute: false); ShaderSource[] shaderSourcesArray = new ShaderSource[] { CreateShaderSource(translatedShader.Program) }; - ShaderInfo info = ShaderInfoBuilder.BuildForCompute(_context, translatedShader.Program.Info); + ShaderInfo info = ShaderInfoBuilder.BuildForCompute( + _context, + translatedShader.Program.Info, + computeState.GetLocalSize()); IProgram hostProgram = _context.Renderer.CreateProgram(shaderSourcesArray, info); cpShader = new CachedShaderProgram(hostProgram, specState, translatedShader.Shader); @@ -425,7 +428,8 @@ namespace Ryujinx.Graphics.Gpu.Shader TranslatorContext lastInVertexPipeline = geometryToCompute ? translatorContexts[4] ?? currentStage : currentStage; - program = lastInVertexPipeline.GenerateVertexPassthroughForCompute(); + (program, ShaderProgramInfo vacInfo) = lastInVertexPipeline.GenerateVertexPassthroughForCompute(); + infoBuilder.AddStageInfoVac(vacInfo); } else { @@ -530,7 +534,7 @@ namespace Ryujinx.Graphics.Gpu.Shader private ShaderAsCompute CreateHostVertexAsComputeProgram(ShaderProgram program, TranslatorContext context, bool tfEnabled) { ShaderSource source = new(program.Code, program.BinaryCode, ShaderStage.Compute, program.Language); - ShaderInfo info = ShaderInfoBuilder.BuildForVertexAsCompute(_context, program.Info, tfEnabled); + ShaderInfo info = ShaderInfoBuilder.BuildForVertexAsCompute(_context, program.Info, context.GetVertexAsComputeInfo(), tfEnabled); return new(_context.Renderer.CreateProgram(new[] { source }, info), program.Info, context.GetResourceReservations()); } @@ -822,16 +826,19 @@ namespace Ryujinx.Graphics.Gpu.Shader /// /// Creates shader translation options with the requested graphics API and flags. - /// The shader language is choosen based on the current configuration and graphics API. + /// The shader language is chosen based on the current configuration and graphics API. /// /// Target graphics API /// Translation flags /// Translation options private static TranslationOptions CreateTranslationOptions(TargetApi api, TranslationFlags flags) { - TargetLanguage lang = GraphicsConfig.EnableSpirvCompilationOnVulkan && api == TargetApi.Vulkan - ? TargetLanguage.Spirv - : TargetLanguage.Glsl; + TargetLanguage lang = api switch + { + TargetApi.OpenGL => TargetLanguage.Glsl, + TargetApi.Vulkan => GraphicsConfig.EnableSpirvCompilationOnVulkan ? TargetLanguage.Spirv : TargetLanguage.Glsl, + TargetApi.Metal => TargetLanguage.Msl, + }; return new TranslationOptions(lang, api, flags); } diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs index 49823562f..e283d0832 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderInfoBuilder.cs @@ -22,6 +22,7 @@ namespace Ryujinx.Graphics.Gpu.Shader ResourceStages.Geometry; private readonly GpuContext _context; + private readonly ComputeSize _computeLocalSize; private int _fragmentOutputMap; @@ -39,9 +40,11 @@ namespace Ryujinx.Graphics.Gpu.Shader /// GPU context that owns the shaders that will be added to the builder /// Indicates if the graphics shader is used with transform feedback enabled /// Indicates that the vertex shader will be emulated on a compute shader - public ShaderInfoBuilder(GpuContext context, bool tfEnabled, bool vertexAsCompute = false) + /// Indicates the local thread size for a compute shader + public ShaderInfoBuilder(GpuContext context, bool tfEnabled, bool vertexAsCompute = false, ComputeSize computeLocalSize = default) { _context = context; + _computeLocalSize = computeLocalSize; _fragmentOutputMap = -1; @@ -95,7 +98,7 @@ namespace Ryujinx.Graphics.Gpu.Shader private void PopulateDescriptorAndUsages(ResourceStages stages, ResourceType type, int setIndex, int start, int count, bool write = false) { AddDescriptor(stages, type, setIndex, start, count); - AddUsage(stages, type, setIndex, start, count, write); + // AddUsage(stages, type, setIndex, start, count, write); } /// @@ -159,6 +162,25 @@ namespace Ryujinx.Graphics.Gpu.Shader AddUsage(info.Images, stages, isImage: true); } + public void AddStageInfoVac(ShaderProgramInfo info) + { + ResourceStages stages = info.Stage switch + { + ShaderStage.Compute => ResourceStages.Compute, + ShaderStage.Vertex => ResourceStages.Vertex, + ShaderStage.TessellationControl => ResourceStages.TessellationControl, + ShaderStage.TessellationEvaluation => ResourceStages.TessellationEvaluation, + ShaderStage.Geometry => ResourceStages.Geometry, + ShaderStage.Fragment => ResourceStages.Fragment, + _ => ResourceStages.None, + }; + + AddUsage(info.CBuffers, stages, isStorage: false); + AddUsage(info.SBuffers, stages, isStorage: true); + AddUsage(info.Textures, stages, isImage: false); + AddUsage(info.Images, stages, isImage: true); + } + /// /// Adds a resource descriptor to the list of descriptors. /// @@ -361,14 +383,7 @@ namespace Ryujinx.Graphics.Gpu.Shader ResourceLayout resourceLayout = new(descriptors.AsReadOnly(), usages.AsReadOnly()); - if (pipeline.HasValue) - { - return new ShaderInfo(_fragmentOutputMap, resourceLayout, pipeline.Value, fromCache); - } - else - { - return new ShaderInfo(_fragmentOutputMap, resourceLayout, fromCache); - } + return new ShaderInfo(_fragmentOutputMap, resourceLayout, _computeLocalSize, pipeline, fromCache); } /// @@ -378,14 +393,16 @@ namespace Ryujinx.Graphics.Gpu.Shader /// Shaders from the disk cache /// Optional pipeline for background compilation /// Indicates if the graphics shader is used with transform feedback enabled + /// Compute local thread size /// Shader information public static ShaderInfo BuildForCache( GpuContext context, IEnumerable programs, ProgramPipelineState? pipeline, - bool tfEnabled) + bool tfEnabled, + ComputeSize computeLocalSize) { - ShaderInfoBuilder builder = new(context, tfEnabled); + ShaderInfoBuilder builder = new(context, tfEnabled, computeLocalSize: computeLocalSize); foreach (CachedShaderStage program in programs) { @@ -403,11 +420,12 @@ namespace Ryujinx.Graphics.Gpu.Shader /// /// GPU context that owns the shader /// Compute shader information + /// Compute local thread size /// True if the compute shader comes from a disk cache, false otherwise /// Shader information - public static ShaderInfo BuildForCompute(GpuContext context, ShaderProgramInfo info, bool fromCache = false) + public static ShaderInfo BuildForCompute(GpuContext context, ShaderProgramInfo info, ComputeSize computeLocalSize, bool fromCache = false) { - ShaderInfoBuilder builder = new(context, tfEnabled: false, vertexAsCompute: false); + ShaderInfoBuilder builder = new(context, tfEnabled: false, vertexAsCompute: false, computeLocalSize: computeLocalSize); builder.AddStageInfo(info); @@ -422,10 +440,11 @@ namespace Ryujinx.Graphics.Gpu.Shader /// Indicates if the graphics shader is used with transform feedback enabled /// True if the compute shader comes from a disk cache, false otherwise /// Shader information - public static ShaderInfo BuildForVertexAsCompute(GpuContext context, ShaderProgramInfo info, bool tfEnabled, bool fromCache = false) + public static ShaderInfo BuildForVertexAsCompute(GpuContext context, ShaderProgramInfo info, ShaderProgramInfo info2, bool tfEnabled, bool fromCache = false) { - ShaderInfoBuilder builder = new(context, tfEnabled, vertexAsCompute: true); + ShaderInfoBuilder builder = new(context, tfEnabled, vertexAsCompute: true, computeLocalSize: ComputeSize.VtgAsCompute); + builder.AddStageInfoVac(info2); builder.AddStageInfo(info, vertexAsCompute: true); return builder.Build(null, fromCache); diff --git a/src/Ryujinx.Graphics.Metal/Auto.cs b/src/Ryujinx.Graphics.Metal/Auto.cs new file mode 100644 index 000000000..7e79ecbc3 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Auto.cs @@ -0,0 +1,146 @@ +using System; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; + +namespace Ryujinx.Graphics.Metal +{ + interface IAuto + { + bool HasCommandBufferDependency(CommandBufferScoped cbs); + + void IncrementReferenceCount(); + void DecrementReferenceCount(int cbIndex); + void DecrementReferenceCount(); + } + + interface IAutoPrivate : IAuto + { + void AddCommandBufferDependencies(CommandBufferScoped cbs); + } + + [SupportedOSPlatform("macos")] + class Auto : IAutoPrivate, IDisposable where T : IDisposable + { + private int _referenceCount; + private T _value; + + private readonly BitMap _cbOwnership; + private readonly MultiFenceHolder _waitable; + + private bool _disposed; + private bool _destroyed; + + public Auto(T value) + { + _referenceCount = 1; + _value = value; + _cbOwnership = new BitMap(CommandBufferPool.MaxCommandBuffers); + } + + public Auto(T value, MultiFenceHolder waitable) : this(value) + { + _waitable = waitable; + } + + public T Get(CommandBufferScoped cbs, int offset, int size, bool write = false) + { + _waitable?.AddBufferUse(cbs.CommandBufferIndex, offset, size, write); + return Get(cbs); + } + + public T GetUnsafe() + { + return _value; + } + + public T Get(CommandBufferScoped cbs) + { + if (!_destroyed) + { + AddCommandBufferDependencies(cbs); + } + + return _value; + } + + public bool HasCommandBufferDependency(CommandBufferScoped cbs) + { + return _cbOwnership.IsSet(cbs.CommandBufferIndex); + } + + public bool HasRentedCommandBufferDependency(CommandBufferPool cbp) + { + return _cbOwnership.AnySet(); + } + + public void AddCommandBufferDependencies(CommandBufferScoped cbs) + { + // We don't want to add a reference to this object to the command buffer + // more than once, so if we detect that the command buffer already has ownership + // of this object, then we can just return without doing anything else. + if (_cbOwnership.Set(cbs.CommandBufferIndex)) + { + if (_waitable != null) + { + cbs.AddWaitable(_waitable); + } + + cbs.AddDependant(this); + } + } + + public bool TryIncrementReferenceCount() + { + int lastValue; + do + { + lastValue = _referenceCount; + + if (lastValue == 0) + { + return false; + } + } + while (Interlocked.CompareExchange(ref _referenceCount, lastValue + 1, lastValue) != lastValue); + + return true; + } + + public void IncrementReferenceCount() + { + if (Interlocked.Increment(ref _referenceCount) == 1) + { + Interlocked.Decrement(ref _referenceCount); + throw new InvalidOperationException("Attempted to increment the reference count of an object that was already destroyed."); + } + } + + public void DecrementReferenceCount(int cbIndex) + { + _cbOwnership.Clear(cbIndex); + DecrementReferenceCount(); + } + + public void DecrementReferenceCount() + { + if (Interlocked.Decrement(ref _referenceCount) == 0) + { + _value.Dispose(); + _value = default; + _destroyed = true; + } + + Debug.Assert(_referenceCount >= 0); + } + + public void Dispose() + { + if (!_disposed) + { + DecrementReferenceCount(); + _disposed = true; + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/BackgroundResources.cs b/src/Ryujinx.Graphics.Metal/BackgroundResources.cs new file mode 100644 index 000000000..8bf6b92bd --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/BackgroundResources.cs @@ -0,0 +1,107 @@ +using SharpMetal.Metal; +using System; +using System.Collections.Generic; +using System.Runtime.Versioning; +using System.Threading; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + class BackgroundResource : IDisposable + { + private readonly MetalRenderer _renderer; + + private CommandBufferPool _pool; + private PersistentFlushBuffer _flushBuffer; + + public BackgroundResource(MetalRenderer renderer) + { + _renderer = renderer; + } + + public CommandBufferPool GetPool() + { + if (_pool == null) + { + MTLCommandQueue queue = _renderer.BackgroundQueue; + _pool = new CommandBufferPool(queue, true); + _pool.Initialize(null); // TODO: Proper encoder factory for background render/compute + } + + return _pool; + } + + public PersistentFlushBuffer GetFlushBuffer() + { + _flushBuffer ??= new PersistentFlushBuffer(_renderer); + + return _flushBuffer; + } + + public void Dispose() + { + _pool?.Dispose(); + _flushBuffer?.Dispose(); + } + } + + [SupportedOSPlatform("macos")] + class BackgroundResources : IDisposable + { + private readonly MetalRenderer _renderer; + + private readonly Dictionary _resources; + + public BackgroundResources(MetalRenderer renderer) + { + _renderer = renderer; + + _resources = new Dictionary(); + } + + private void Cleanup() + { + lock (_resources) + { + foreach (KeyValuePair tuple in _resources) + { + if (!tuple.Key.IsAlive) + { + tuple.Value.Dispose(); + _resources.Remove(tuple.Key); + } + } + } + } + + public BackgroundResource Get() + { + Thread thread = Thread.CurrentThread; + + lock (_resources) + { + if (!_resources.TryGetValue(thread, out BackgroundResource resource)) + { + Cleanup(); + + resource = new BackgroundResource(_renderer); + + _resources[thread] = resource; + } + + return resource; + } + } + + public void Dispose() + { + lock (_resources) + { + foreach (var resource in _resources.Values) + { + resource.Dispose(); + } + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/BitMap.cs b/src/Ryujinx.Graphics.Metal/BitMap.cs new file mode 100644 index 000000000..4ddc438c1 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/BitMap.cs @@ -0,0 +1,157 @@ +namespace Ryujinx.Graphics.Metal +{ + readonly struct BitMap + { + public const int IntSize = 64; + + private const int IntShift = 6; + private const int IntMask = IntSize - 1; + + private readonly long[] _masks; + + public BitMap(int count) + { + _masks = new long[(count + IntMask) / IntSize]; + } + + public bool AnySet() + { + for (int i = 0; i < _masks.Length; i++) + { + if (_masks[i] != 0) + { + return true; + } + } + + return false; + } + + public bool IsSet(int bit) + { + int wordIndex = bit >> IntShift; + int wordBit = bit & IntMask; + + long wordMask = 1L << wordBit; + + return (_masks[wordIndex] & wordMask) != 0; + } + + public bool IsSet(int start, int end) + { + if (start == end) + { + return IsSet(start); + } + + int startIndex = start >> IntShift; + int startBit = start & IntMask; + long startMask = -1L << startBit; + + int endIndex = end >> IntShift; + int endBit = end & IntMask; + long endMask = (long)(ulong.MaxValue >> (IntMask - endBit)); + + if (startIndex == endIndex) + { + return (_masks[startIndex] & startMask & endMask) != 0; + } + + if ((_masks[startIndex] & startMask) != 0) + { + return true; + } + + for (int i = startIndex + 1; i < endIndex; i++) + { + if (_masks[i] != 0) + { + return true; + } + } + + if ((_masks[endIndex] & endMask) != 0) + { + return true; + } + + return false; + } + + public bool Set(int bit) + { + int wordIndex = bit >> IntShift; + int wordBit = bit & IntMask; + + long wordMask = 1L << wordBit; + + if ((_masks[wordIndex] & wordMask) != 0) + { + return false; + } + + _masks[wordIndex] |= wordMask; + + return true; + } + + public void SetRange(int start, int end) + { + if (start == end) + { + Set(start); + return; + } + + int startIndex = start >> IntShift; + int startBit = start & IntMask; + long startMask = -1L << startBit; + + int endIndex = end >> IntShift; + int endBit = end & IntMask; + long endMask = (long)(ulong.MaxValue >> (IntMask - endBit)); + + if (startIndex == endIndex) + { + _masks[startIndex] |= startMask & endMask; + } + else + { + _masks[startIndex] |= startMask; + + for (int i = startIndex + 1; i < endIndex; i++) + { + _masks[i] |= -1; + } + + _masks[endIndex] |= endMask; + } + } + + public void Clear(int bit) + { + int wordIndex = bit >> IntShift; + int wordBit = bit & IntMask; + + long wordMask = 1L << wordBit; + + _masks[wordIndex] &= ~wordMask; + } + + public void Clear() + { + for (int i = 0; i < _masks.Length; i++) + { + _masks[i] = 0; + } + } + + public void ClearInt(int start, int end) + { + for (int i = start; i <= end; i++) + { + _masks[i] = 0; + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/BufferHolder.cs b/src/Ryujinx.Graphics.Metal/BufferHolder.cs new file mode 100644 index 000000000..cc86a403f --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/BufferHolder.cs @@ -0,0 +1,385 @@ +using Ryujinx.Graphics.GAL; +using SharpMetal.Metal; +using System; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Threading; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + class BufferHolder : IDisposable + { + private CacheByRange _cachedConvertedBuffers; + + public int Size { get; } + + private readonly IntPtr _map; + private readonly MetalRenderer _renderer; + private readonly Pipeline _pipeline; + + private readonly MultiFenceHolder _waitable; + private readonly Auto _buffer; + + private readonly ReaderWriterLockSlim _flushLock; + private FenceHolder _flushFence; + private int _flushWaiting; + + public BufferHolder(MetalRenderer renderer, Pipeline pipeline, MTLBuffer buffer, int size) + { + _renderer = renderer; + _pipeline = pipeline; + _map = buffer.Contents; + _waitable = new MultiFenceHolder(size); + _buffer = new Auto(new(buffer), _waitable); + + _flushLock = new ReaderWriterLockSlim(); + + Size = size; + } + + public Auto GetBuffer() + { + return _buffer; + } + + public Auto GetBuffer(bool isWrite) + { + if (isWrite) + { + SignalWrite(0, Size); + } + + return _buffer; + } + + public Auto GetBuffer(int offset, int size, bool isWrite) + { + if (isWrite) + { + SignalWrite(offset, size); + } + + return _buffer; + } + + public void SignalWrite(int offset, int size) + { + if (offset == 0 && size == Size) + { + _cachedConvertedBuffers.Clear(); + } + else + { + _cachedConvertedBuffers.ClearRange(offset, size); + } + } + + private void ClearFlushFence() + { + // Assumes _flushLock is held as writer. + + if (_flushFence != null) + { + if (_flushWaiting == 0) + { + _flushFence.Put(); + } + + _flushFence = null; + } + } + + private void WaitForFlushFence() + { + if (_flushFence == null) + { + return; + } + + // If storage has changed, make sure the fence has been reached so that the data is in place. + _flushLock.ExitReadLock(); + _flushLock.EnterWriteLock(); + + if (_flushFence != null) + { + var fence = _flushFence; + Interlocked.Increment(ref _flushWaiting); + + // Don't wait in the lock. + + _flushLock.ExitWriteLock(); + + fence.Wait(); + + _flushLock.EnterWriteLock(); + + if (Interlocked.Decrement(ref _flushWaiting) == 0) + { + fence.Put(); + } + + _flushFence = null; + } + + // Assumes the _flushLock is held as reader, returns in same state. + _flushLock.ExitWriteLock(); + _flushLock.EnterReadLock(); + } + + public PinnedSpan GetData(int offset, int size) + { + _flushLock.EnterReadLock(); + + WaitForFlushFence(); + + Span result; + + if (_map != IntPtr.Zero) + { + result = GetDataStorage(offset, size); + + // Need to be careful here, the buffer can't be unmapped while the data is being used. + _buffer.IncrementReferenceCount(); + + _flushLock.ExitReadLock(); + + return PinnedSpan.UnsafeFromSpan(result, _buffer.DecrementReferenceCount); + } + + throw new InvalidOperationException("The buffer is not mapped"); + } + + public unsafe Span GetDataStorage(int offset, int size) + { + int mappingSize = Math.Min(size, Size - offset); + + if (_map != IntPtr.Zero) + { + return new Span((void*)(_map + offset), mappingSize); + } + + throw new InvalidOperationException("The buffer is not mapped."); + } + + public unsafe void SetData(int offset, ReadOnlySpan data, CommandBufferScoped? cbs = null, bool allowCbsWait = true) + { + int dataSize = Math.Min(data.Length, Size - offset); + if (dataSize == 0) + { + return; + } + + if (_map != IntPtr.Zero) + { + // If persistently mapped, set the data directly if the buffer is not currently in use. + bool isRented = _buffer.HasRentedCommandBufferDependency(_renderer.CommandBufferPool); + + // If the buffer is rented, take a little more time and check if the use overlaps this handle. + bool needsFlush = isRented && _waitable.IsBufferRangeInUse(offset, dataSize, false); + + if (!needsFlush) + { + WaitForFences(offset, dataSize); + + data[..dataSize].CopyTo(new Span((void*)(_map + offset), dataSize)); + + SignalWrite(offset, dataSize); + + return; + } + } + + if (cbs != null && + cbs.Value.Encoders.CurrentEncoderType == EncoderType.Render && + !(_buffer.HasCommandBufferDependency(cbs.Value) && + _waitable.IsBufferRangeInUse(cbs.Value.CommandBufferIndex, offset, dataSize))) + { + // If the buffer hasn't been used on the command buffer yet, try to preload the data. + // This avoids ending and beginning render passes on each buffer data upload. + + cbs = _pipeline.GetPreloadCommandBuffer(); + } + + if (allowCbsWait) + { + _renderer.BufferManager.StagingBuffer.PushData(_renderer.CommandBufferPool, cbs, this, offset, data); + } + else + { + bool rentCbs = cbs == null; + if (rentCbs) + { + cbs = _renderer.CommandBufferPool.Rent(); + } + + if (!_renderer.BufferManager.StagingBuffer.TryPushData(cbs.Value, this, offset, data)) + { + // Need to do a slow upload. + BufferHolder srcHolder = _renderer.BufferManager.Create(dataSize); + srcHolder.SetDataUnchecked(0, data); + + var srcBuffer = srcHolder.GetBuffer(); + var dstBuffer = this.GetBuffer(true); + + Copy(cbs.Value, srcBuffer, dstBuffer, 0, offset, dataSize); + + srcHolder.Dispose(); + } + + if (rentCbs) + { + cbs.Value.Dispose(); + } + } + } + + public unsafe void SetDataUnchecked(int offset, ReadOnlySpan data) + { + int dataSize = Math.Min(data.Length, Size - offset); + if (dataSize == 0) + { + return; + } + + if (_map != IntPtr.Zero) + { + data[..dataSize].CopyTo(new Span((void*)(_map + offset), dataSize)); + } + } + + public void SetDataUnchecked(int offset, ReadOnlySpan data) where T : unmanaged + { + SetDataUnchecked(offset, MemoryMarshal.AsBytes(data)); + } + + public static void Copy( + CommandBufferScoped cbs, + Auto src, + Auto dst, + int srcOffset, + int dstOffset, + int size, + bool registerSrcUsage = true) + { + var srcBuffer = registerSrcUsage ? src.Get(cbs, srcOffset, size).Value : src.GetUnsafe().Value; + var dstbuffer = dst.Get(cbs, dstOffset, size, true).Value; + + cbs.Encoders.EnsureBlitEncoder().CopyFromBuffer( + srcBuffer, + (ulong)srcOffset, + dstbuffer, + (ulong)dstOffset, + (ulong)size); + } + + public void WaitForFences() + { + _waitable.WaitForFences(); + } + + public void WaitForFences(int offset, int size) + { + _waitable.WaitForFences(offset, size); + } + + private bool BoundToRange(int offset, ref int size) + { + if (offset >= Size) + { + return false; + } + + size = Math.Min(Size - offset, size); + + return true; + } + + public Auto GetBufferI8ToI16(CommandBufferScoped cbs, int offset, int size) + { + if (!BoundToRange(offset, ref size)) + { + return null; + } + + var key = new I8ToI16CacheKey(_renderer); + + if (!_cachedConvertedBuffers.TryGetValue(offset, size, key, out var holder)) + { + holder = _renderer.BufferManager.Create((size * 2 + 3) & ~3); + + _renderer.HelperShader.ConvertI8ToI16(cbs, this, holder, offset, size); + + key.SetBuffer(holder.GetBuffer()); + + _cachedConvertedBuffers.Add(offset, size, key, holder); + } + + return holder.GetBuffer(); + } + + public Auto GetBufferTopologyConversion(CommandBufferScoped cbs, int offset, int size, IndexBufferPattern pattern, int indexSize) + { + if (!BoundToRange(offset, ref size)) + { + return null; + } + + var key = new TopologyConversionCacheKey(_renderer, pattern, indexSize); + + if (!_cachedConvertedBuffers.TryGetValue(offset, size, key, out var holder)) + { + // The destination index size is always I32. + + int indexCount = size / indexSize; + + int convertedCount = pattern.GetConvertedCount(indexCount); + + holder = _renderer.BufferManager.Create(convertedCount * 4); + + _renderer.HelperShader.ConvertIndexBuffer(cbs, this, holder, pattern, indexSize, offset, indexCount); + + key.SetBuffer(holder.GetBuffer()); + + _cachedConvertedBuffers.Add(offset, size, key, holder); + } + + return holder.GetBuffer(); + } + + public bool TryGetCachedConvertedBuffer(int offset, int size, ICacheKey key, out BufferHolder holder) + { + return _cachedConvertedBuffers.TryGetValue(offset, size, key, out holder); + } + + public void AddCachedConvertedBuffer(int offset, int size, ICacheKey key, BufferHolder holder) + { + _cachedConvertedBuffers.Add(offset, size, key, holder); + } + + public void AddCachedConvertedBufferDependency(int offset, int size, ICacheKey key, Dependency dependency) + { + _cachedConvertedBuffers.AddDependency(offset, size, key, dependency); + } + + public void RemoveCachedConvertedBuffer(int offset, int size, ICacheKey key) + { + _cachedConvertedBuffers.Remove(offset, size, key); + } + + + public void Dispose() + { + _pipeline.FlushCommandsIfWeightExceeding(_buffer, (ulong)Size); + + _buffer.Dispose(); + _cachedConvertedBuffers.Dispose(); + + _flushLock.EnterWriteLock(); + + ClearFlushFence(); + + _flushLock.ExitWriteLock(); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/BufferManager.cs b/src/Ryujinx.Graphics.Metal/BufferManager.cs new file mode 100644 index 000000000..07a686223 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/BufferManager.cs @@ -0,0 +1,237 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.GAL; +using SharpMetal.Metal; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + readonly struct ScopedTemporaryBuffer : IDisposable + { + private readonly BufferManager _bufferManager; + private readonly bool _isReserved; + + public readonly BufferRange Range; + public readonly BufferHolder Holder; + + public BufferHandle Handle => Range.Handle; + public int Offset => Range.Offset; + + public ScopedTemporaryBuffer(BufferManager bufferManager, BufferHolder holder, BufferHandle handle, int offset, int size, bool isReserved) + { + _bufferManager = bufferManager; + + Range = new BufferRange(handle, offset, size); + Holder = holder; + + _isReserved = isReserved; + } + + public void Dispose() + { + if (!_isReserved) + { + _bufferManager.Delete(Range.Handle); + } + } + } + + [SupportedOSPlatform("macos")] + class BufferManager : IDisposable + { + private readonly IdList _buffers; + + private readonly MTLDevice _device; + private readonly MetalRenderer _renderer; + private readonly Pipeline _pipeline; + + public int BufferCount { get; private set; } + + public StagingBuffer StagingBuffer { get; } + + public BufferManager(MTLDevice device, MetalRenderer renderer, Pipeline pipeline) + { + _device = device; + _renderer = renderer; + _pipeline = pipeline; + _buffers = new IdList(); + + StagingBuffer = new StagingBuffer(_renderer, this); + } + + public BufferHandle Create(nint pointer, int size) + { + // TODO: This is the wrong Metal method, we need no-copy which SharpMetal isn't giving us. + var buffer = _device.NewBuffer(pointer, (ulong)size, MTLResourceOptions.ResourceStorageModeShared); + + if (buffer == IntPtr.Zero) + { + Logger.Error?.PrintMsg(LogClass.Gpu, $"Failed to create buffer with size 0x{size:X}, and pointer 0x{pointer:X}."); + + return BufferHandle.Null; + } + + var holder = new BufferHolder(_renderer, _pipeline, buffer, size); + + BufferCount++; + + ulong handle64 = (uint)_buffers.Add(holder); + + return Unsafe.As(ref handle64); + } + + public BufferHandle CreateWithHandle(int size) + { + return CreateWithHandle(size, out _); + } + + public BufferHandle CreateWithHandle(int size, out BufferHolder holder) + { + holder = Create(size); + + if (holder == null) + { + return BufferHandle.Null; + } + + BufferCount++; + + ulong handle64 = (uint)_buffers.Add(holder); + + return Unsafe.As(ref handle64); + } + + public ScopedTemporaryBuffer ReserveOrCreate(CommandBufferScoped cbs, int size) + { + StagingBufferReserved? result = StagingBuffer.TryReserveData(cbs, size); + + if (result.HasValue) + { + return new ScopedTemporaryBuffer(this, result.Value.Buffer, StagingBuffer.Handle, result.Value.Offset, result.Value.Size, true); + } + else + { + // Create a temporary buffer. + BufferHandle handle = CreateWithHandle(size, out BufferHolder holder); + + return new ScopedTemporaryBuffer(this, holder, handle, 0, size, false); + } + } + + public BufferHolder Create(int size) + { + var buffer = _device.NewBuffer((ulong)size, MTLResourceOptions.ResourceStorageModeShared); + + if (buffer != IntPtr.Zero) + { + return new BufferHolder(_renderer, _pipeline, buffer, size); + } + + Logger.Error?.PrintMsg(LogClass.Gpu, $"Failed to create buffer with size 0x{size:X}."); + + return null; + } + + public Auto GetBuffer(BufferHandle handle, bool isWrite, out int size) + { + if (TryGetBuffer(handle, out var holder)) + { + size = holder.Size; + return holder.GetBuffer(isWrite); + } + + size = 0; + return null; + } + + public Auto GetBuffer(BufferHandle handle, int offset, int size, bool isWrite) + { + if (TryGetBuffer(handle, out var holder)) + { + return holder.GetBuffer(offset, size, isWrite); + } + + return null; + } + + public Auto GetBuffer(BufferHandle handle, bool isWrite) + { + if (TryGetBuffer(handle, out var holder)) + { + return holder.GetBuffer(isWrite); + } + + return null; + } + + public Auto GetBufferI8ToI16(CommandBufferScoped cbs, BufferHandle handle, int offset, int size) + { + if (TryGetBuffer(handle, out var holder)) + { + return holder.GetBufferI8ToI16(cbs, offset, size); + } + + return null; + } + + public Auto GetBufferTopologyConversion(CommandBufferScoped cbs, BufferHandle handle, int offset, int size, IndexBufferPattern pattern, int indexSize) + { + if (TryGetBuffer(handle, out var holder)) + { + return holder.GetBufferTopologyConversion(cbs, offset, size, pattern, indexSize); + } + + return null; + } + + public PinnedSpan GetData(BufferHandle handle, int offset, int size) + { + if (TryGetBuffer(handle, out var holder)) + { + return holder.GetData(offset, size); + } + + return new PinnedSpan(); + } + + public void SetData(BufferHandle handle, int offset, ReadOnlySpan data) where T : unmanaged + { + SetData(handle, offset, MemoryMarshal.Cast(data), null); + } + + public void SetData(BufferHandle handle, int offset, ReadOnlySpan data, CommandBufferScoped? cbs) + { + if (TryGetBuffer(handle, out var holder)) + { + holder.SetData(offset, data, cbs); + } + } + + public void Delete(BufferHandle handle) + { + if (TryGetBuffer(handle, out var holder)) + { + holder.Dispose(); + _buffers.Remove((int)Unsafe.As(ref handle)); + } + } + + private bool TryGetBuffer(BufferHandle handle, out BufferHolder holder) + { + return _buffers.TryGetValue((int)Unsafe.As(ref handle), out holder); + } + + public void Dispose() + { + StagingBuffer.Dispose(); + + foreach (var buffer in _buffers) + { + buffer.Dispose(); + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/BufferUsageBitmap.cs b/src/Ryujinx.Graphics.Metal/BufferUsageBitmap.cs new file mode 100644 index 000000000..379e27407 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/BufferUsageBitmap.cs @@ -0,0 +1,85 @@ +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + internal class BufferUsageBitmap + { + private readonly BitMap _bitmap; + private readonly int _size; + private readonly int _granularity; + private readonly int _bits; + private readonly int _writeBitOffset; + + private readonly int _intsPerCb; + private readonly int _bitsPerCb; + + public BufferUsageBitmap(int size, int granularity) + { + _size = size; + _granularity = granularity; + + // There are two sets of bits - one for read tracking, and the other for write. + int bits = (size + (granularity - 1)) / granularity; + _writeBitOffset = bits; + _bits = bits << 1; + + _intsPerCb = (_bits + (BitMap.IntSize - 1)) / BitMap.IntSize; + _bitsPerCb = _intsPerCb * BitMap.IntSize; + + _bitmap = new BitMap(_bitsPerCb * CommandBufferPool.MaxCommandBuffers); + } + + public void Add(int cbIndex, int offset, int size, bool write) + { + if (size == 0) + { + return; + } + + // Some usages can be out of bounds (vertex buffer on amd), so bound if necessary. + if (offset + size > _size) + { + size = _size - offset; + } + + int cbBase = cbIndex * _bitsPerCb + (write ? _writeBitOffset : 0); + int start = cbBase + offset / _granularity; + int end = cbBase + (offset + size - 1) / _granularity; + + _bitmap.SetRange(start, end); + } + + public bool OverlapsWith(int cbIndex, int offset, int size, bool write = false) + { + if (size == 0) + { + return false; + } + + int cbBase = cbIndex * _bitsPerCb + (write ? _writeBitOffset : 0); + int start = cbBase + offset / _granularity; + int end = cbBase + (offset + size - 1) / _granularity; + + return _bitmap.IsSet(start, end); + } + + public bool OverlapsWith(int offset, int size, bool write) + { + for (int i = 0; i < CommandBufferPool.MaxCommandBuffers; i++) + { + if (OverlapsWith(i, offset, size, write)) + { + return true; + } + } + + return false; + } + + public void Clear(int cbIndex) + { + _bitmap.ClearInt(cbIndex * _intsPerCb, (cbIndex + 1) * _intsPerCb - 1); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/CacheByRange.cs b/src/Ryujinx.Graphics.Metal/CacheByRange.cs new file mode 100644 index 000000000..76515808f --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/CacheByRange.cs @@ -0,0 +1,294 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + interface ICacheKey : IDisposable + { + bool KeyEqual(ICacheKey other); + } + + [SupportedOSPlatform("macos")] + struct I8ToI16CacheKey : ICacheKey + { + // Used to notify the pipeline that bindings have invalidated on dispose. + // private readonly MetalRenderer _renderer; + // private Auto _buffer; + + public I8ToI16CacheKey(MetalRenderer renderer) + { + // _renderer = renderer; + // _buffer = null; + } + + public readonly bool KeyEqual(ICacheKey other) + { + return other is I8ToI16CacheKey; + } + + public readonly void SetBuffer(Auto buffer) + { + // _buffer = buffer; + } + + public readonly void Dispose() + { + // TODO: Tell pipeline buffer is dirty! + // _renderer.PipelineInternal.DirtyIndexBuffer(_buffer); + } + } + + [SupportedOSPlatform("macos")] + readonly struct TopologyConversionCacheKey : ICacheKey + { + private readonly IndexBufferPattern _pattern; + private readonly int _indexSize; + + // Used to notify the pipeline that bindings have invalidated on dispose. + // private readonly MetalRenderer _renderer; + // private Auto _buffer; + + public TopologyConversionCacheKey(MetalRenderer renderer, IndexBufferPattern pattern, int indexSize) + { + // _renderer = renderer; + // _buffer = null; + _pattern = pattern; + _indexSize = indexSize; + } + + public readonly bool KeyEqual(ICacheKey other) + { + return other is TopologyConversionCacheKey entry && + entry._pattern == _pattern && + entry._indexSize == _indexSize; + } + + public void SetBuffer(Auto buffer) + { + // _buffer = buffer; + } + + public readonly void Dispose() + { + // TODO: Tell pipeline buffer is dirty! + // _renderer.PipelineInternal.DirtyVertexBuffer(_buffer); + } + } + + [SupportedOSPlatform("macos")] + readonly struct Dependency + { + private readonly BufferHolder _buffer; + private readonly int _offset; + private readonly int _size; + private readonly ICacheKey _key; + + public Dependency(BufferHolder buffer, int offset, int size, ICacheKey key) + { + _buffer = buffer; + _offset = offset; + _size = size; + _key = key; + } + + public void RemoveFromOwner() + { + _buffer.RemoveCachedConvertedBuffer(_offset, _size, _key); + } + } + + [SupportedOSPlatform("macos")] + struct CacheByRange where T : IDisposable + { + private struct Entry + { + public readonly ICacheKey Key; + public readonly T Value; + public List DependencyList; + + public Entry(ICacheKey key, T value) + { + Key = key; + Value = value; + DependencyList = null; + } + + public readonly void InvalidateDependencies() + { + if (DependencyList != null) + { + foreach (Dependency dependency in DependencyList) + { + dependency.RemoveFromOwner(); + } + + DependencyList.Clear(); + } + } + } + + private Dictionary> _ranges; + + public void Add(int offset, int size, ICacheKey key, T value) + { + List entries = GetEntries(offset, size); + + entries.Add(new Entry(key, value)); + } + + public void AddDependency(int offset, int size, ICacheKey key, Dependency dependency) + { + List entries = GetEntries(offset, size); + + for (int i = 0; i < entries.Count; i++) + { + Entry entry = entries[i]; + + if (entry.Key.KeyEqual(key)) + { + if (entry.DependencyList == null) + { + entry.DependencyList = new List(); + entries[i] = entry; + } + + entry.DependencyList.Add(dependency); + + break; + } + } + } + + public void Remove(int offset, int size, ICacheKey key) + { + List entries = GetEntries(offset, size); + + for (int i = 0; i < entries.Count; i++) + { + Entry entry = entries[i]; + + if (entry.Key.KeyEqual(key)) + { + entries.RemoveAt(i--); + + DestroyEntry(entry); + } + } + + if (entries.Count == 0) + { + _ranges.Remove(PackRange(offset, size)); + } + } + + public bool TryGetValue(int offset, int size, ICacheKey key, out T value) + { + List entries = GetEntries(offset, size); + + foreach (Entry entry in entries) + { + if (entry.Key.KeyEqual(key)) + { + value = entry.Value; + + return true; + } + } + + value = default; + return false; + } + + public void Clear() + { + if (_ranges != null) + { + foreach (List entries in _ranges.Values) + { + foreach (Entry entry in entries) + { + DestroyEntry(entry); + } + } + + _ranges.Clear(); + _ranges = null; + } + } + + public readonly void ClearRange(int offset, int size) + { + if (_ranges != null && _ranges.Count > 0) + { + int end = offset + size; + + List toRemove = null; + + foreach (KeyValuePair> range in _ranges) + { + (int rOffset, int rSize) = UnpackRange(range.Key); + + int rEnd = rOffset + rSize; + + if (rEnd > offset && rOffset < end) + { + List entries = range.Value; + + foreach (Entry entry in entries) + { + DestroyEntry(entry); + } + + (toRemove ??= new List()).Add(range.Key); + } + } + + if (toRemove != null) + { + foreach (ulong range in toRemove) + { + _ranges.Remove(range); + } + } + } + } + + private List GetEntries(int offset, int size) + { + _ranges ??= new Dictionary>(); + + ulong key = PackRange(offset, size); + + if (!_ranges.TryGetValue(key, out List value)) + { + value = new List(); + _ranges.Add(key, value); + } + + return value; + } + + private static void DestroyEntry(Entry entry) + { + entry.Key.Dispose(); + entry.Value?.Dispose(); + entry.InvalidateDependencies(); + } + + private static ulong PackRange(int offset, int size) + { + return (uint)offset | ((ulong)size << 32); + } + + private static (int offset, int size) UnpackRange(ulong range) + { + return ((int)range, (int)(range >> 32)); + } + + public void Dispose() + { + Clear(); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/CommandBufferEncoder.cs b/src/Ryujinx.Graphics.Metal/CommandBufferEncoder.cs new file mode 100644 index 000000000..ec4150030 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/CommandBufferEncoder.cs @@ -0,0 +1,170 @@ +using Ryujinx.Graphics.Metal; +using SharpMetal.Metal; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.Versioning; + +interface IEncoderFactory +{ + MTLRenderCommandEncoder CreateRenderCommandEncoder(); + MTLComputeCommandEncoder CreateComputeCommandEncoder(); +} + +/// +/// Tracks active encoder object for a command buffer. +/// +[SupportedOSPlatform("macos")] +class CommandBufferEncoder +{ + public EncoderType CurrentEncoderType { get; private set; } = EncoderType.None; + + public MTLBlitCommandEncoder BlitEncoder => new(CurrentEncoder.Value); + + public MTLComputeCommandEncoder ComputeEncoder => new(CurrentEncoder.Value); + + public MTLRenderCommandEncoder RenderEncoder => new(CurrentEncoder.Value); + + internal MTLCommandEncoder? CurrentEncoder { get; private set; } + + private MTLCommandBuffer _commandBuffer; + private IEncoderFactory _encoderFactory; + + public void Initialize(MTLCommandBuffer commandBuffer, IEncoderFactory encoderFactory) + { + _commandBuffer = commandBuffer; + _encoderFactory = encoderFactory; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MTLRenderCommandEncoder EnsureRenderEncoder() + { + if (CurrentEncoderType != EncoderType.Render) + { + return BeginRenderPass(); + } + + return RenderEncoder; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MTLBlitCommandEncoder EnsureBlitEncoder() + { + if (CurrentEncoderType != EncoderType.Blit) + { + return BeginBlitPass(); + } + + return BlitEncoder; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public MTLComputeCommandEncoder EnsureComputeEncoder() + { + if (CurrentEncoderType != EncoderType.Compute) + { + return BeginComputePass(); + } + + return ComputeEncoder; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetRenderEncoder(out MTLRenderCommandEncoder encoder) + { + if (CurrentEncoderType != EncoderType.Render) + { + encoder = default; + return false; + } + + encoder = RenderEncoder; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetBlitEncoder(out MTLBlitCommandEncoder encoder) + { + if (CurrentEncoderType != EncoderType.Blit) + { + encoder = default; + return false; + } + + encoder = BlitEncoder; + return true; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public bool TryGetComputeEncoder(out MTLComputeCommandEncoder encoder) + { + if (CurrentEncoderType != EncoderType.Compute) + { + encoder = default; + return false; + } + + encoder = ComputeEncoder; + return true; + } + + public void EndCurrentPass() + { + if (CurrentEncoder != null) + { + switch (CurrentEncoderType) + { + case EncoderType.Blit: + BlitEncoder.EndEncoding(); + CurrentEncoder = null; + break; + case EncoderType.Compute: + ComputeEncoder.EndEncoding(); + CurrentEncoder = null; + break; + case EncoderType.Render: + RenderEncoder.EndEncoding(); + CurrentEncoder = null; + break; + default: + throw new InvalidOperationException(); + } + + CurrentEncoderType = EncoderType.None; + } + } + + private MTLRenderCommandEncoder BeginRenderPass() + { + EndCurrentPass(); + + var renderCommandEncoder = _encoderFactory.CreateRenderCommandEncoder(); + + CurrentEncoder = renderCommandEncoder; + CurrentEncoderType = EncoderType.Render; + + return renderCommandEncoder; + } + + private MTLBlitCommandEncoder BeginBlitPass() + { + EndCurrentPass(); + + using var descriptor = new MTLBlitPassDescriptor(); + var blitCommandEncoder = _commandBuffer.BlitCommandEncoder(descriptor); + + CurrentEncoder = blitCommandEncoder; + CurrentEncoderType = EncoderType.Blit; + return blitCommandEncoder; + } + + private MTLComputeCommandEncoder BeginComputePass() + { + EndCurrentPass(); + + var computeCommandEncoder = _encoderFactory.CreateComputeCommandEncoder(); + + CurrentEncoder = computeCommandEncoder; + CurrentEncoderType = EncoderType.Compute; + return computeCommandEncoder; + } +} diff --git a/src/Ryujinx.Graphics.Metal/CommandBufferPool.cs b/src/Ryujinx.Graphics.Metal/CommandBufferPool.cs new file mode 100644 index 000000000..5e7576b37 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/CommandBufferPool.cs @@ -0,0 +1,289 @@ +using SharpMetal.Metal; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; +using System.Threading; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + class CommandBufferPool : IDisposable + { + public const int MaxCommandBuffers = 16; + + private readonly int _totalCommandBuffers; + private readonly int _totalCommandBuffersMask; + private readonly MTLCommandQueue _queue; + private readonly Thread _owner; + private IEncoderFactory _defaultEncoderFactory; + + public bool OwnedByCurrentThread => _owner == Thread.CurrentThread; + + [SupportedOSPlatform("macos")] + private struct ReservedCommandBuffer + { + public bool InUse; + public bool InConsumption; + public int SubmissionCount; + public MTLCommandBuffer CommandBuffer; + public CommandBufferEncoder Encoders; + public FenceHolder Fence; + + public List Dependants; + public List Waitables; + + public void Use(MTLCommandQueue queue, IEncoderFactory stateManager) + { + MTLCommandBufferDescriptor descriptor = new(); +#if DEBUG + descriptor.ErrorOptions = MTLCommandBufferErrorOption.EncoderExecutionStatus; +#endif + + CommandBuffer = queue.CommandBuffer(descriptor); + Fence = new FenceHolder(CommandBuffer); + + Encoders.Initialize(CommandBuffer, stateManager); + + InUse = true; + } + + public void Initialize() + { + Dependants = new List(); + Waitables = new List(); + Encoders = new CommandBufferEncoder(); + } + } + + private readonly ReservedCommandBuffer[] _commandBuffers; + + private readonly int[] _queuedIndexes; + private int _queuedIndexesPtr; + private int _queuedCount; + private int _inUseCount; + + public CommandBufferPool(MTLCommandQueue queue, bool isLight = false) + { + _queue = queue; + _owner = Thread.CurrentThread; + + _totalCommandBuffers = isLight ? 2 : MaxCommandBuffers; + _totalCommandBuffersMask = _totalCommandBuffers - 1; + + _commandBuffers = new ReservedCommandBuffer[_totalCommandBuffers]; + + _queuedIndexes = new int[_totalCommandBuffers]; + _queuedIndexesPtr = 0; + _queuedCount = 0; + } + + public void Initialize(IEncoderFactory encoderFactory) + { + _defaultEncoderFactory = encoderFactory; + + for (int i = 0; i < _totalCommandBuffers; i++) + { + _commandBuffers[i].Initialize(); + WaitAndDecrementRef(i); + } + } + + public void AddDependant(int cbIndex, IAuto dependant) + { + dependant.IncrementReferenceCount(); + _commandBuffers[cbIndex].Dependants.Add(dependant); + } + + public void AddWaitable(MultiFenceHolder waitable) + { + lock (_commandBuffers) + { + for (int i = 0; i < _totalCommandBuffers; i++) + { + ref var entry = ref _commandBuffers[i]; + + if (entry.InConsumption) + { + AddWaitable(i, waitable); + } + } + } + } + + public void AddInUseWaitable(MultiFenceHolder waitable) + { + lock (_commandBuffers) + { + for (int i = 0; i < _totalCommandBuffers; i++) + { + ref var entry = ref _commandBuffers[i]; + + if (entry.InUse) + { + AddWaitable(i, waitable); + } + } + } + } + + public void AddWaitable(int cbIndex, MultiFenceHolder waitable) + { + ref var entry = ref _commandBuffers[cbIndex]; + if (waitable.AddFence(cbIndex, entry.Fence)) + { + entry.Waitables.Add(waitable); + } + } + + public bool IsFenceOnRentedCommandBuffer(FenceHolder fence) + { + lock (_commandBuffers) + { + for (int i = 0; i < _totalCommandBuffers; i++) + { + ref var entry = ref _commandBuffers[i]; + + if (entry.InUse && entry.Fence == fence) + { + return true; + } + } + } + + return false; + } + + public FenceHolder GetFence(int cbIndex) + { + return _commandBuffers[cbIndex].Fence; + } + + public int GetSubmissionCount(int cbIndex) + { + return _commandBuffers[cbIndex].SubmissionCount; + } + + private int FreeConsumed(bool wait) + { + int freeEntry = 0; + + while (_queuedCount > 0) + { + int index = _queuedIndexes[_queuedIndexesPtr]; + + ref var entry = ref _commandBuffers[index]; + + if (wait || !entry.InConsumption || entry.Fence.IsSignaled()) + { + WaitAndDecrementRef(index); + + wait = false; + freeEntry = index; + + _queuedCount--; + _queuedIndexesPtr = (_queuedIndexesPtr + 1) % _totalCommandBuffers; + } + else + { + break; + } + } + + return freeEntry; + } + + public CommandBufferScoped ReturnAndRent(CommandBufferScoped cbs) + { + Return(cbs); + return Rent(); + } + + public CommandBufferScoped Rent() + { + lock (_commandBuffers) + { + int cursor = FreeConsumed(_inUseCount + _queuedCount == _totalCommandBuffers); + + for (int i = 0; i < _totalCommandBuffers; i++) + { + ref var entry = ref _commandBuffers[cursor]; + + if (!entry.InUse && !entry.InConsumption) + { + entry.Use(_queue, _defaultEncoderFactory); + + _inUseCount++; + + return new CommandBufferScoped(this, entry.CommandBuffer, entry.Encoders, cursor); + } + + cursor = (cursor + 1) & _totalCommandBuffersMask; + } + } + + throw new InvalidOperationException($"Out of command buffers (In use: {_inUseCount}, queued: {_queuedCount}, total: {_totalCommandBuffers})"); + } + + public void Return(CommandBufferScoped cbs) + { + // Ensure the encoder is committed. + cbs.Encoders.EndCurrentPass(); + + lock (_commandBuffers) + { + int cbIndex = cbs.CommandBufferIndex; + + ref var entry = ref _commandBuffers[cbIndex]; + + Debug.Assert(entry.InUse); + Debug.Assert(entry.CommandBuffer.NativePtr == cbs.CommandBuffer.NativePtr); + entry.InUse = false; + entry.InConsumption = true; + entry.SubmissionCount++; + _inUseCount--; + + var commandBuffer = entry.CommandBuffer; + commandBuffer.Commit(); + + int ptr = (_queuedIndexesPtr + _queuedCount) % _totalCommandBuffers; + _queuedIndexes[ptr] = cbIndex; + _queuedCount++; + } + } + + private void WaitAndDecrementRef(int cbIndex) + { + ref var entry = ref _commandBuffers[cbIndex]; + + if (entry.InConsumption) + { + entry.Fence.Wait(); + entry.InConsumption = false; + } + + foreach (var dependant in entry.Dependants) + { + dependant.DecrementReferenceCount(cbIndex); + } + + foreach (var waitable in entry.Waitables) + { + waitable.RemoveFence(cbIndex); + waitable.RemoveBufferUses(cbIndex); + } + + entry.Dependants.Clear(); + entry.Waitables.Clear(); + entry.Fence?.Dispose(); + } + + public void Dispose() + { + for (int i = 0; i < _totalCommandBuffers; i++) + { + WaitAndDecrementRef(i); + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/CommandBufferScoped.cs b/src/Ryujinx.Graphics.Metal/CommandBufferScoped.cs new file mode 100644 index 000000000..822f69b46 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/CommandBufferScoped.cs @@ -0,0 +1,43 @@ +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + readonly struct CommandBufferScoped : IDisposable + { + private readonly CommandBufferPool _pool; + public MTLCommandBuffer CommandBuffer { get; } + public CommandBufferEncoder Encoders { get; } + public int CommandBufferIndex { get; } + + public CommandBufferScoped(CommandBufferPool pool, MTLCommandBuffer commandBuffer, CommandBufferEncoder encoders, int commandBufferIndex) + { + _pool = pool; + CommandBuffer = commandBuffer; + Encoders = encoders; + CommandBufferIndex = commandBufferIndex; + } + + public void AddDependant(IAuto dependant) + { + _pool.AddDependant(CommandBufferIndex, dependant); + } + + public void AddWaitable(MultiFenceHolder waitable) + { + _pool.AddWaitable(CommandBufferIndex, waitable); + } + + public FenceHolder GetFence() + { + return _pool.GetFence(CommandBufferIndex); + } + + public void Dispose() + { + _pool?.Return(this); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/Constants.cs b/src/Ryujinx.Graphics.Metal/Constants.cs new file mode 100644 index 000000000..43baf722a --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Constants.cs @@ -0,0 +1,41 @@ +namespace Ryujinx.Graphics.Metal +{ + static class Constants + { + public const int MaxShaderStages = 5; + public const int MaxVertexBuffers = 16; + public const int MaxUniformBuffersPerStage = 18; + public const int MaxStorageBuffersPerStage = 16; + public const int MaxTexturesPerStage = 64; + public const int MaxImagesPerStage = 16; + + public const int MaxUniformBufferBindings = MaxUniformBuffersPerStage * MaxShaderStages; + public const int MaxStorageBufferBindings = MaxStorageBuffersPerStage * MaxShaderStages; + public const int MaxTextureBindings = MaxTexturesPerStage * MaxShaderStages; + public const int MaxImageBindings = MaxImagesPerStage * MaxShaderStages; + public const int MaxColorAttachments = 8; + public const int MaxViewports = 16; + // TODO: Check this value + public const int MaxVertexAttributes = 31; + + public const int MinResourceAlignment = 16; + + // Must match constants set in shader generation + public const uint ZeroBufferIndex = MaxVertexBuffers; + public const uint BaseSetIndex = MaxVertexBuffers + 1; + + public const uint ConstantBuffersIndex = BaseSetIndex; + public const uint StorageBuffersIndex = BaseSetIndex + 1; + public const uint TexturesIndex = BaseSetIndex + 2; + public const uint ImagesIndex = BaseSetIndex + 3; + + public const uint ConstantBuffersSetIndex = 0; + public const uint StorageBuffersSetIndex = 1; + public const uint TexturesSetIndex = 2; + public const uint ImagesSetIndex = 3; + + public const uint MaximumBufferArgumentTableEntries = 31; + + public const uint MaximumExtraSets = MaximumBufferArgumentTableEntries - ImagesIndex; + } +} diff --git a/src/Ryujinx.Graphics.Metal/CounterEvent.cs b/src/Ryujinx.Graphics.Metal/CounterEvent.cs new file mode 100644 index 000000000..46b04997e --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/CounterEvent.cs @@ -0,0 +1,22 @@ +using Ryujinx.Graphics.GAL; + +namespace Ryujinx.Graphics.Metal +{ + class CounterEvent : ICounterEvent + { + public CounterEvent() + { + Invalid = false; + } + + public bool Invalid { get; set; } + public bool ReserveForHostAccess() + { + return true; + } + + public void Flush() { } + + public void Dispose() { } + } +} diff --git a/src/Ryujinx.Graphics.Metal/DepthStencilCache.cs b/src/Ryujinx.Graphics.Metal/DepthStencilCache.cs new file mode 100644 index 000000000..bb6e4c180 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/DepthStencilCache.cs @@ -0,0 +1,68 @@ +using Ryujinx.Graphics.Metal.State; +using SharpMetal.Metal; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + class DepthStencilCache : StateCache + { + private readonly MTLDevice _device; + + public DepthStencilCache(MTLDevice device) + { + _device = device; + } + + protected override DepthStencilUid GetHash(DepthStencilUid descriptor) + { + return descriptor; + } + + protected override MTLDepthStencilState CreateValue(DepthStencilUid descriptor) + { + // Create descriptors + + ref StencilUid frontUid = ref descriptor.FrontFace; + + using var frontFaceStencil = new MTLStencilDescriptor + { + StencilFailureOperation = frontUid.StencilFailureOperation, + DepthFailureOperation = frontUid.DepthFailureOperation, + DepthStencilPassOperation = frontUid.DepthStencilPassOperation, + StencilCompareFunction = frontUid.StencilCompareFunction, + ReadMask = frontUid.ReadMask, + WriteMask = frontUid.WriteMask + }; + + ref StencilUid backUid = ref descriptor.BackFace; + + using var backFaceStencil = new MTLStencilDescriptor + { + StencilFailureOperation = backUid.StencilFailureOperation, + DepthFailureOperation = backUid.DepthFailureOperation, + DepthStencilPassOperation = backUid.DepthStencilPassOperation, + StencilCompareFunction = backUid.StencilCompareFunction, + ReadMask = backUid.ReadMask, + WriteMask = backUid.WriteMask + }; + + var mtlDescriptor = new MTLDepthStencilDescriptor + { + DepthCompareFunction = descriptor.DepthCompareFunction, + DepthWriteEnabled = descriptor.DepthWriteEnabled + }; + + if (descriptor.StencilTestEnabled) + { + mtlDescriptor.BackFaceStencil = backFaceStencil; + mtlDescriptor.FrontFaceStencil = frontFaceStencil; + } + + using (mtlDescriptor) + { + return _device.NewDepthStencilState(mtlDescriptor); + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/DisposableBuffer.cs b/src/Ryujinx.Graphics.Metal/DisposableBuffer.cs new file mode 100644 index 000000000..a2d2247c4 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/DisposableBuffer.cs @@ -0,0 +1,26 @@ +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + readonly struct DisposableBuffer : IDisposable + { + public MTLBuffer Value { get; } + + public DisposableBuffer(MTLBuffer buffer) + { + Value = buffer; + } + + public void Dispose() + { + if (Value != IntPtr.Zero) + { + Value.SetPurgeableState(MTLPurgeableState.Empty); + Value.Dispose(); + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/DisposableSampler.cs b/src/Ryujinx.Graphics.Metal/DisposableSampler.cs new file mode 100644 index 000000000..ba041be89 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/DisposableSampler.cs @@ -0,0 +1,22 @@ +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + readonly struct DisposableSampler : IDisposable + { + public MTLSamplerState Value { get; } + + public DisposableSampler(MTLSamplerState sampler) + { + Value = sampler; + } + + public void Dispose() + { + Value.Dispose(); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/Effects/IPostProcessingEffect.cs b/src/Ryujinx.Graphics.Metal/Effects/IPostProcessingEffect.cs new file mode 100644 index 000000000..d575d521f --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Effects/IPostProcessingEffect.cs @@ -0,0 +1,10 @@ +using System; + +namespace Ryujinx.Graphics.Metal.Effects +{ + internal interface IPostProcessingEffect : IDisposable + { + const int LocalGroupSize = 64; + Texture Run(Texture view, int width, int height); + } +} diff --git a/src/Ryujinx.Graphics.Metal/Effects/IScalingFilter.cs b/src/Ryujinx.Graphics.Metal/Effects/IScalingFilter.cs new file mode 100644 index 000000000..19f1a3c3d --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Effects/IScalingFilter.cs @@ -0,0 +1,18 @@ +using Ryujinx.Graphics.GAL; +using System; + +namespace Ryujinx.Graphics.Metal.Effects +{ + internal interface IScalingFilter : IDisposable + { + float Level { get; set; } + void Run( + Texture view, + Texture destinationTexture, + Format format, + int width, + int height, + Extents2D source, + Extents2D destination); + } +} diff --git a/src/Ryujinx.Graphics.Metal/EncoderResources.cs b/src/Ryujinx.Graphics.Metal/EncoderResources.cs new file mode 100644 index 000000000..562500d76 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/EncoderResources.cs @@ -0,0 +1,63 @@ +using SharpMetal.Metal; +using System.Collections.Generic; + +namespace Ryujinx.Graphics.Metal +{ + public struct RenderEncoderBindings + { + public List Resources = new(); + public List VertexBuffers = new(); + public List FragmentBuffers = new(); + + public RenderEncoderBindings() { } + + public readonly void Clear() + { + Resources.Clear(); + VertexBuffers.Clear(); + FragmentBuffers.Clear(); + } + } + + public struct ComputeEncoderBindings + { + public List Resources = new(); + public List Buffers = new(); + + public ComputeEncoderBindings() { } + + public readonly void Clear() + { + Resources.Clear(); + Buffers.Clear(); + } + } + + public struct BufferResource + { + public MTLBuffer Buffer; + public ulong Offset; + public ulong Binding; + + public BufferResource(MTLBuffer buffer, ulong offset, ulong binding) + { + Buffer = buffer; + Offset = offset; + Binding = binding; + } + } + + public struct Resource + { + public MTLResource MtlResource; + public MTLResourceUsage ResourceUsage; + public MTLRenderStages Stages; + + public Resource(MTLResource resource, MTLResourceUsage resourceUsage, MTLRenderStages stages) + { + MtlResource = resource; + ResourceUsage = resourceUsage; + Stages = stages; + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/EncoderState.cs b/src/Ryujinx.Graphics.Metal/EncoderState.cs new file mode 100644 index 000000000..34de168a6 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/EncoderState.cs @@ -0,0 +1,206 @@ +using Ryujinx.Common.Memory; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Metal.State; +using Ryujinx.Graphics.Shader; +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [Flags] + enum DirtyFlags + { + None = 0, + RenderPipeline = 1 << 0, + ComputePipeline = 1 << 1, + DepthStencil = 1 << 2, + DepthClamp = 1 << 3, + DepthBias = 1 << 4, + CullMode = 1 << 5, + FrontFace = 1 << 6, + StencilRef = 1 << 7, + Viewports = 1 << 8, + Scissors = 1 << 9, + Uniforms = 1 << 10, + Storages = 1 << 11, + Textures = 1 << 12, + Images = 1 << 13, + + ArgBuffers = Uniforms | Storages | Textures | Images, + + RenderAll = RenderPipeline | DepthStencil | DepthClamp | DepthBias | CullMode | FrontFace | StencilRef | Viewports | Scissors | ArgBuffers, + ComputeAll = ComputePipeline | ArgBuffers, + All = RenderAll | ComputeAll, + } + + record struct BufferRef + { + public Auto Buffer; + public BufferRange? Range; + + public BufferRef(Auto buffer) + { + Buffer = buffer; + } + + public BufferRef(Auto buffer, ref BufferRange range) + { + Buffer = buffer; + Range = range; + } + } + + record struct TextureRef + { + public ShaderStage Stage; + public TextureBase Storage; + public Auto Sampler; + public Format ImageFormat; + + public TextureRef(ShaderStage stage, TextureBase storage, Auto sampler) + { + Stage = stage; + Storage = storage; + Sampler = sampler; + } + } + + record struct ImageRef + { + public ShaderStage Stage; + public Texture Storage; + + public ImageRef(ShaderStage stage, Texture storage) + { + Stage = stage; + Storage = storage; + } + } + + struct PredrawState + { + public MTLCullMode CullMode; + public DepthStencilUid DepthStencilUid; + public PrimitiveTopology Topology; + public MTLViewport[] Viewports; + } + + struct RenderTargetCopy + { + public MTLScissorRect[] Scissors; + public Texture DepthStencil; + public Texture[] RenderTargets; + } + + [SupportedOSPlatform("macos")] + class EncoderState + { + public Program RenderProgram = null; + public Program ComputeProgram = null; + + public PipelineState Pipeline; + public DepthStencilUid DepthStencilUid; + + public readonly record struct ArrayRef(ShaderStage Stage, T Array); + + public readonly BufferRef[] UniformBufferRefs = new BufferRef[Constants.MaxUniformBufferBindings]; + public readonly BufferRef[] StorageBufferRefs = new BufferRef[Constants.MaxStorageBufferBindings]; + public readonly TextureRef[] TextureRefs = new TextureRef[Constants.MaxTextureBindings * 2]; + public readonly ImageRef[] ImageRefs = new ImageRef[Constants.MaxImageBindings * 2]; + + public ArrayRef[] TextureArrayRefs = []; + public ArrayRef[] ImageArrayRefs = []; + + public ArrayRef[] TextureArrayExtraRefs = []; + public ArrayRef[] ImageArrayExtraRefs = []; + + public IndexBufferState IndexBuffer = default; + + public MTLDepthClipMode DepthClipMode = MTLDepthClipMode.Clip; + + public float DepthBias; + public float SlopeScale; + public float Clamp; + + public int BackRefValue = 0; + public int FrontRefValue = 0; + + public PrimitiveTopology Topology = PrimitiveTopology.Triangles; + public MTLCullMode CullMode = MTLCullMode.None; + public MTLWinding Winding = MTLWinding.CounterClockwise; + public bool CullBoth = false; + + public MTLViewport[] Viewports = new MTLViewport[Constants.MaxViewports]; + public MTLScissorRect[] Scissors = new MTLScissorRect[Constants.MaxViewports]; + + // Changes to attachments take recreation! + public Texture DepthStencil; + public Texture[] RenderTargets = new Texture[Constants.MaxColorAttachments]; + public ITexture PreMaskDepthStencil = default; + public ITexture[] PreMaskRenderTargets; + public bool FramebufferUsingColorWriteMask; + + public Array8 StoredBlend; + public ColorF BlendColor = new(); + + public readonly VertexBufferState[] VertexBuffers = new VertexBufferState[Constants.MaxVertexBuffers]; + public readonly VertexAttribDescriptor[] VertexAttribs = new VertexAttribDescriptor[Constants.MaxVertexAttributes]; + // Dirty flags + public DirtyFlags Dirty = DirtyFlags.None; + + // Only to be used for present + public bool ClearLoadAction = false; + + public RenderEncoderBindings RenderEncoderBindings = new(); + public ComputeEncoderBindings ComputeEncoderBindings = new(); + + public EncoderState() + { + Pipeline.Initialize(); + DepthStencilUid.DepthCompareFunction = MTLCompareFunction.Always; + } + + public RenderTargetCopy InheritForClear(EncoderState other, bool depth, int singleIndex = -1) + { + // Inherit render target related information without causing a render encoder split. + + var oldState = new RenderTargetCopy + { + Scissors = other.Scissors, + RenderTargets = other.RenderTargets, + DepthStencil = other.DepthStencil + }; + + Scissors = other.Scissors; + RenderTargets = other.RenderTargets; + DepthStencil = other.DepthStencil; + + Pipeline.ColorBlendAttachmentStateCount = other.Pipeline.ColorBlendAttachmentStateCount; + Pipeline.Internal.ColorBlendState = other.Pipeline.Internal.ColorBlendState; + Pipeline.DepthStencilFormat = other.Pipeline.DepthStencilFormat; + + ref var blendStates = ref Pipeline.Internal.ColorBlendState; + + // Mask out irrelevant attachments. + for (int i = 0; i < blendStates.Length; i++) + { + if (depth || (singleIndex != -1 && singleIndex != i)) + { + blendStates[i].WriteMask = MTLColorWriteMask.None; + } + } + + return oldState; + } + + public void Restore(RenderTargetCopy copy) + { + Scissors = copy.Scissors; + RenderTargets = copy.RenderTargets; + DepthStencil = copy.DepthStencil; + + Pipeline.Internal.ResetColorState(); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/EncoderStateManager.cs b/src/Ryujinx.Graphics.Metal/EncoderStateManager.cs new file mode 100644 index 000000000..0093ac148 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/EncoderStateManager.cs @@ -0,0 +1,1788 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Metal.State; +using Ryujinx.Graphics.Shader; +using SharpMetal.Metal; +using System; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using BufferAssignment = Ryujinx.Graphics.GAL.BufferAssignment; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + struct EncoderStateManager : IDisposable + { + private const int ArrayGrowthSize = 16; + + private readonly MTLDevice _device; + private readonly Pipeline _pipeline; + private readonly BufferManager _bufferManager; + + private readonly DepthStencilCache _depthStencilCache; + private readonly MTLDepthStencilState _defaultState; + + private readonly EncoderState _mainState = new(); + private EncoderState _currentState; + + public readonly IndexBufferState IndexBuffer => _currentState.IndexBuffer; + public readonly PrimitiveTopology Topology => _currentState.Topology; + public readonly Texture[] RenderTargets => _currentState.RenderTargets; + public readonly Texture DepthStencil => _currentState.DepthStencil; + public readonly ComputeSize ComputeLocalSize => _currentState.ComputeProgram.ComputeLocalSize; + + // RGBA32F is the biggest format + private const int ZeroBufferSize = 4 * 4; + private readonly BufferHandle _zeroBuffer; + + public unsafe EncoderStateManager(MTLDevice device, BufferManager bufferManager, Pipeline pipeline) + { + _device = device; + _pipeline = pipeline; + _bufferManager = bufferManager; + + _depthStencilCache = new(device); + _currentState = _mainState; + + _defaultState = _depthStencilCache.GetOrCreate(_currentState.DepthStencilUid); + + // Zero buffer + byte[] zeros = new byte[ZeroBufferSize]; + fixed (byte* ptr = zeros) + { + _zeroBuffer = _bufferManager.Create((IntPtr)ptr, ZeroBufferSize); + } + } + + public readonly void Dispose() + { + _depthStencilCache.Dispose(); + } + + private readonly void SignalDirty(DirtyFlags flags) + { + _currentState.Dirty |= flags; + } + + public readonly void SignalRenderDirty() + { + SignalDirty(DirtyFlags.RenderAll); + } + + public readonly void SignalComputeDirty() + { + SignalDirty(DirtyFlags.ComputeAll); + } + + public EncoderState SwapState(EncoderState state, DirtyFlags flags = DirtyFlags.All) + { + _currentState = state ?? _mainState; + + SignalDirty(flags); + + return _mainState; + } + + public PredrawState SavePredrawState() + { + return new PredrawState + { + CullMode = _currentState.CullMode, + DepthStencilUid = _currentState.DepthStencilUid, + Topology = _currentState.Topology, + Viewports = _currentState.Viewports.ToArray(), + }; + } + + public readonly void RestorePredrawState(PredrawState state) + { + _currentState.CullMode = state.CullMode; + _currentState.DepthStencilUid = state.DepthStencilUid; + _currentState.Topology = state.Topology; + _currentState.Viewports = state.Viewports; + + SignalDirty(DirtyFlags.CullMode | DirtyFlags.DepthStencil | DirtyFlags.Viewports); + } + + public readonly void SetClearLoadAction(bool clear) + { + _currentState.ClearLoadAction = clear; + } + + public readonly void DirtyTextures() + { + SignalDirty(DirtyFlags.Textures); + } + + public readonly void DirtyImages() + { + SignalDirty(DirtyFlags.Images); + } + + public readonly MTLRenderCommandEncoder CreateRenderCommandEncoder() + { + // Initialise Pass & State + using var renderPassDescriptor = new MTLRenderPassDescriptor(); + + for (int i = 0; i < Constants.MaxColorAttachments; i++) + { + if (_currentState.RenderTargets[i] is Texture tex) + { + var passAttachment = renderPassDescriptor.ColorAttachments.Object((ulong)i); + tex.PopulateRenderPassAttachment(passAttachment); + passAttachment.LoadAction = _currentState.ClearLoadAction ? MTLLoadAction.Clear : MTLLoadAction.Load; + passAttachment.StoreAction = MTLStoreAction.Store; + } + } + + var depthAttachment = renderPassDescriptor.DepthAttachment; + var stencilAttachment = renderPassDescriptor.StencilAttachment; + + if (_currentState.DepthStencil != null) + { + switch (_currentState.DepthStencil.GetHandle().PixelFormat) + { + // Depth Only Attachment + case MTLPixelFormat.Depth16Unorm: + case MTLPixelFormat.Depth32Float: + depthAttachment.Texture = _currentState.DepthStencil.GetHandle(); + depthAttachment.LoadAction = MTLLoadAction.Load; + depthAttachment.StoreAction = MTLStoreAction.Store; + break; + + // Stencil Only Attachment + case MTLPixelFormat.Stencil8: + stencilAttachment.Texture = _currentState.DepthStencil.GetHandle(); + stencilAttachment.LoadAction = MTLLoadAction.Load; + stencilAttachment.StoreAction = MTLStoreAction.Store; + break; + + // Combined Attachment + case MTLPixelFormat.Depth24UnormStencil8: + case MTLPixelFormat.Depth32FloatStencil8: + depthAttachment.Texture = _currentState.DepthStencil.GetHandle(); + depthAttachment.LoadAction = MTLLoadAction.Load; + depthAttachment.StoreAction = MTLStoreAction.Store; + + stencilAttachment.Texture = _currentState.DepthStencil.GetHandle(); + stencilAttachment.LoadAction = MTLLoadAction.Load; + stencilAttachment.StoreAction = MTLStoreAction.Store; + break; + default: + Logger.Error?.PrintMsg(LogClass.Gpu, $"Unsupported Depth/Stencil Format: {_currentState.DepthStencil.GetHandle().PixelFormat}!"); + break; + } + } + + // Initialise Encoder + var renderCommandEncoder = _pipeline.CommandBuffer.RenderCommandEncoder(renderPassDescriptor); + + return renderCommandEncoder; + } + + public readonly MTLComputeCommandEncoder CreateComputeCommandEncoder() + { + using var descriptor = new MTLComputePassDescriptor(); + var computeCommandEncoder = _pipeline.CommandBuffer.ComputeCommandEncoder(descriptor); + + return computeCommandEncoder; + } + + public readonly void RenderResourcesPrepass() + { + _currentState.RenderEncoderBindings.Clear(); + + if ((_currentState.Dirty & DirtyFlags.RenderPipeline) != 0) + { + SetVertexBuffers(_currentState.VertexBuffers, ref _currentState.RenderEncoderBindings); + } + + if ((_currentState.Dirty & DirtyFlags.Uniforms) != 0) + { + UpdateAndBind(_currentState.RenderProgram, Constants.ConstantBuffersSetIndex, ref _currentState.RenderEncoderBindings); + } + + if ((_currentState.Dirty & DirtyFlags.Storages) != 0) + { + UpdateAndBind(_currentState.RenderProgram, Constants.StorageBuffersSetIndex, ref _currentState.RenderEncoderBindings); + } + + if ((_currentState.Dirty & DirtyFlags.Textures) != 0) + { + UpdateAndBind(_currentState.RenderProgram, Constants.TexturesSetIndex, ref _currentState.RenderEncoderBindings); + } + + if ((_currentState.Dirty & DirtyFlags.Images) != 0) + { + UpdateAndBind(_currentState.RenderProgram, Constants.ImagesSetIndex, ref _currentState.RenderEncoderBindings); + } + } + + public readonly void ComputeResourcesPrepass() + { + _currentState.ComputeEncoderBindings.Clear(); + + if ((_currentState.Dirty & DirtyFlags.Uniforms) != 0) + { + UpdateAndBind(_currentState.ComputeProgram, Constants.ConstantBuffersSetIndex, ref _currentState.ComputeEncoderBindings); + } + + if ((_currentState.Dirty & DirtyFlags.Storages) != 0) + { + UpdateAndBind(_currentState.ComputeProgram, Constants.StorageBuffersSetIndex, ref _currentState.ComputeEncoderBindings); + } + + if ((_currentState.Dirty & DirtyFlags.Textures) != 0) + { + UpdateAndBind(_currentState.ComputeProgram, Constants.TexturesSetIndex, ref _currentState.ComputeEncoderBindings); + } + + if ((_currentState.Dirty & DirtyFlags.Images) != 0) + { + UpdateAndBind(_currentState.ComputeProgram, Constants.ImagesSetIndex, ref _currentState.ComputeEncoderBindings); + } + } + + public void RebindRenderState(MTLRenderCommandEncoder renderCommandEncoder) + { + if ((_currentState.Dirty & DirtyFlags.RenderPipeline) != 0) + { + SetRenderPipelineState(renderCommandEncoder); + } + + if ((_currentState.Dirty & DirtyFlags.DepthStencil) != 0) + { + SetDepthStencilState(renderCommandEncoder); + } + + if ((_currentState.Dirty & DirtyFlags.DepthClamp) != 0) + { + SetDepthClamp(renderCommandEncoder); + } + + if ((_currentState.Dirty & DirtyFlags.DepthBias) != 0) + { + SetDepthBias(renderCommandEncoder); + } + + if ((_currentState.Dirty & DirtyFlags.CullMode) != 0) + { + SetCullMode(renderCommandEncoder); + } + + if ((_currentState.Dirty & DirtyFlags.FrontFace) != 0) + { + SetFrontFace(renderCommandEncoder); + } + + if ((_currentState.Dirty & DirtyFlags.StencilRef) != 0) + { + SetStencilRefValue(renderCommandEncoder); + } + + if ((_currentState.Dirty & DirtyFlags.Viewports) != 0) + { + SetViewports(renderCommandEncoder); + } + + if ((_currentState.Dirty & DirtyFlags.Scissors) != 0) + { + SetScissors(renderCommandEncoder); + } + + foreach (var resource in _currentState.RenderEncoderBindings.Resources) + { + renderCommandEncoder.UseResource(resource.MtlResource, resource.ResourceUsage, resource.Stages); + } + + foreach (var buffer in _currentState.RenderEncoderBindings.VertexBuffers) + { + renderCommandEncoder.SetVertexBuffer(buffer.Buffer, buffer.Offset, buffer.Binding); + } + + foreach (var buffer in _currentState.RenderEncoderBindings.FragmentBuffers) + { + renderCommandEncoder.SetFragmentBuffer(buffer.Buffer, buffer.Offset, buffer.Binding); + } + + _currentState.Dirty &= ~DirtyFlags.RenderAll; + } + + public readonly void RebindComputeState(MTLComputeCommandEncoder computeCommandEncoder) + { + if ((_currentState.Dirty & DirtyFlags.ComputePipeline) != 0) + { + SetComputePipelineState(computeCommandEncoder); + } + + foreach (var resource in _currentState.ComputeEncoderBindings.Resources) + { + computeCommandEncoder.UseResource(resource.MtlResource, resource.ResourceUsage); + } + + foreach (var buffer in _currentState.ComputeEncoderBindings.Buffers) + { + computeCommandEncoder.SetBuffer(buffer.Buffer, buffer.Offset, buffer.Binding); + } + + _currentState.Dirty &= ~DirtyFlags.ComputeAll; + } + + private readonly void SetRenderPipelineState(MTLRenderCommandEncoder renderCommandEncoder) + { + MTLRenderPipelineState pipelineState = _currentState.Pipeline.CreateRenderPipeline(_device, _currentState.RenderProgram); + + renderCommandEncoder.SetRenderPipelineState(pipelineState); + + renderCommandEncoder.SetBlendColor( + _currentState.BlendColor.Red, + _currentState.BlendColor.Green, + _currentState.BlendColor.Blue, + _currentState.BlendColor.Alpha); + } + + private readonly void SetComputePipelineState(MTLComputeCommandEncoder computeCommandEncoder) + { + if (_currentState.ComputeProgram == null) + { + return; + } + + var pipelineState = PipelineState.CreateComputePipeline(_device, _currentState.ComputeProgram); + + computeCommandEncoder.SetComputePipelineState(pipelineState); + } + + public readonly void UpdateIndexBuffer(BufferRange buffer, IndexType type) + { + if (buffer.Handle != BufferHandle.Null) + { + _currentState.IndexBuffer = new IndexBufferState(buffer.Handle, buffer.Offset, buffer.Size, type); + } + else + { + _currentState.IndexBuffer = IndexBufferState.Null; + } + } + + public readonly void UpdatePrimitiveTopology(PrimitiveTopology topology) + { + _currentState.Topology = topology; + } + + public readonly void UpdateProgram(IProgram program) + { + Program prg = (Program)program; + + if (prg.VertexFunction == IntPtr.Zero && prg.ComputeFunction == IntPtr.Zero) + { + if (prg.FragmentFunction == IntPtr.Zero) + { + Logger.Error?.PrintMsg(LogClass.Gpu, "No compute function"); + } + else + { + Logger.Error?.PrintMsg(LogClass.Gpu, "No vertex function"); + } + return; + } + + if (prg.VertexFunction != IntPtr.Zero) + { + _currentState.RenderProgram = prg; + + SignalDirty(DirtyFlags.RenderPipeline | DirtyFlags.ArgBuffers); + } + else if (prg.ComputeFunction != IntPtr.Zero) + { + _currentState.ComputeProgram = prg; + + SignalDirty(DirtyFlags.ComputePipeline | DirtyFlags.ArgBuffers); + } + } + + public readonly void UpdateRasterizerDiscard(bool discard) + { + _currentState.Pipeline.RasterizerDiscardEnable = discard; + + SignalDirty(DirtyFlags.RenderPipeline); + } + + public readonly void UpdateRenderTargets(ITexture[] colors, ITexture depthStencil) + { + _currentState.FramebufferUsingColorWriteMask = false; + UpdateRenderTargetsInternal(colors, depthStencil); + } + + public readonly void UpdateRenderTargetColorMasks(ReadOnlySpan componentMask) + { + ref var blendState = ref _currentState.Pipeline.Internal.ColorBlendState; + + for (int i = 0; i < componentMask.Length; i++) + { + bool red = (componentMask[i] & (0x1 << 0)) != 0; + bool green = (componentMask[i] & (0x1 << 1)) != 0; + bool blue = (componentMask[i] & (0x1 << 2)) != 0; + bool alpha = (componentMask[i] & (0x1 << 3)) != 0; + + var mask = MTLColorWriteMask.None; + + mask |= red ? MTLColorWriteMask.Red : 0; + mask |= green ? MTLColorWriteMask.Green : 0; + mask |= blue ? MTLColorWriteMask.Blue : 0; + mask |= alpha ? MTLColorWriteMask.Alpha : 0; + + ref ColorBlendStateUid mtlBlend = ref blendState[i]; + + // When color write mask is 0, remove all blend state to help the pipeline cache. + // Restore it when the mask becomes non-zero. + if (mtlBlend.WriteMask != mask) + { + if (mask == 0) + { + _currentState.StoredBlend[i] = mtlBlend; + + mtlBlend.Swap(new ColorBlendStateUid()); + } + else if (mtlBlend.WriteMask == 0) + { + mtlBlend.Swap(_currentState.StoredBlend[i]); + } + } + + blendState[i].WriteMask = mask; + } + + if (_currentState.FramebufferUsingColorWriteMask) + { + UpdateRenderTargetsInternal(_currentState.PreMaskRenderTargets, _currentState.PreMaskDepthStencil); + } + else + { + // Requires recreating pipeline + if (_pipeline.CurrentEncoderType == EncoderType.Render) + { + _pipeline.EndCurrentPass(); + } + } + } + + private readonly void UpdateRenderTargetsInternal(ITexture[] colors, ITexture depthStencil) + { + // TBDR GPUs don't work properly if the same attachment is bound to multiple targets, + // due to each attachment being a copy of the real attachment, rather than a direct write. + // + // Just try to remove duplicate attachments. + // Save a copy of the array to rebind when mask changes. + + // Look for textures that are masked out. + + ref PipelineState pipeline = ref _currentState.Pipeline; + ref var blendState = ref pipeline.Internal.ColorBlendState; + + pipeline.ColorBlendAttachmentStateCount = (uint)colors.Length; + + for (int i = 0; i < colors.Length; i++) + { + if (colors[i] == null) + { + continue; + } + + var mtlMask = blendState[i].WriteMask; + + for (int j = 0; j < i; j++) + { + // Check each binding for a duplicate binding before it. + + if (colors[i] == colors[j]) + { + // Prefer the binding with no write mask. + + var mtlMask2 = blendState[j].WriteMask; + + if (mtlMask == 0) + { + colors[i] = null; + MaskOut(colors, depthStencil); + } + else if (mtlMask2 == 0) + { + colors[j] = null; + MaskOut(colors, depthStencil); + } + } + } + } + + _currentState.RenderTargets = new Texture[Constants.MaxColorAttachments]; + + for (int i = 0; i < colors.Length; i++) + { + if (colors[i] is not Texture tex) + { + blendState[i].PixelFormat = MTLPixelFormat.Invalid; + + continue; + } + + blendState[i].PixelFormat = tex.GetHandle().PixelFormat; // TODO: cache this + _currentState.RenderTargets[i] = tex; + } + + if (depthStencil is Texture depthTexture) + { + pipeline.DepthStencilFormat = depthTexture.GetHandle().PixelFormat; // TODO: cache this + _currentState.DepthStencil = depthTexture; + } + else if (depthStencil == null) + { + pipeline.DepthStencilFormat = MTLPixelFormat.Invalid; + _currentState.DepthStencil = null; + } + + // Requires recreating pipeline + if (_pipeline.CurrentEncoderType == EncoderType.Render) + { + _pipeline.EndCurrentPass(); + } + } + + private readonly void MaskOut(ITexture[] colors, ITexture depthStencil) + { + if (!_currentState.FramebufferUsingColorWriteMask) + { + _currentState.PreMaskRenderTargets = colors; + _currentState.PreMaskDepthStencil = depthStencil; + } + + // If true, then the framebuffer must be recreated when the mask changes. + _currentState.FramebufferUsingColorWriteMask = true; + } + + public readonly void UpdateVertexAttribs(ReadOnlySpan vertexAttribs) + { + vertexAttribs.CopyTo(_currentState.VertexAttribs); + + // Update the buffers on the pipeline + UpdatePipelineVertexState(_currentState.VertexBuffers, _currentState.VertexAttribs); + + SignalDirty(DirtyFlags.RenderPipeline); + } + + public readonly void UpdateBlendDescriptors(int index, BlendDescriptor blend) + { + ref var blendState = ref _currentState.Pipeline.Internal.ColorBlendState[index]; + + blendState.Enable = blend.Enable; + blendState.AlphaBlendOperation = blend.AlphaOp.Convert(); + blendState.RgbBlendOperation = blend.ColorOp.Convert(); + blendState.SourceAlphaBlendFactor = blend.AlphaSrcFactor.Convert(); + blendState.DestinationAlphaBlendFactor = blend.AlphaDstFactor.Convert(); + blendState.SourceRGBBlendFactor = blend.ColorSrcFactor.Convert(); + blendState.DestinationRGBBlendFactor = blend.ColorDstFactor.Convert(); + + if (blendState.WriteMask == 0) + { + _currentState.StoredBlend[index] = blendState; + + blendState.Swap(new ColorBlendStateUid()); + } + + _currentState.BlendColor = blend.BlendConstant; + + SignalDirty(DirtyFlags.RenderPipeline); + } + + public void UpdateStencilState(StencilTestDescriptor stencilTest) + { + ref DepthStencilUid uid = ref _currentState.DepthStencilUid; + + uid.FrontFace = new StencilUid + { + StencilFailureOperation = stencilTest.FrontSFail.Convert(), + DepthFailureOperation = stencilTest.FrontDpFail.Convert(), + DepthStencilPassOperation = stencilTest.FrontDpPass.Convert(), + StencilCompareFunction = stencilTest.FrontFunc.Convert(), + ReadMask = (uint)stencilTest.FrontFuncMask, + WriteMask = (uint)stencilTest.FrontMask + }; + + uid.BackFace = new StencilUid + { + StencilFailureOperation = stencilTest.BackSFail.Convert(), + DepthFailureOperation = stencilTest.BackDpFail.Convert(), + DepthStencilPassOperation = stencilTest.BackDpPass.Convert(), + StencilCompareFunction = stencilTest.BackFunc.Convert(), + ReadMask = (uint)stencilTest.BackFuncMask, + WriteMask = (uint)stencilTest.BackMask + }; + + uid.StencilTestEnabled = stencilTest.TestEnable; + + UpdateStencilRefValue(stencilTest.FrontFuncRef, stencilTest.BackFuncRef); + + SignalDirty(DirtyFlags.DepthStencil); + } + + public readonly void UpdateDepthState(DepthTestDescriptor depthTest) + { + ref DepthStencilUid uid = ref _currentState.DepthStencilUid; + + uid.DepthCompareFunction = depthTest.TestEnable ? depthTest.Func.Convert() : MTLCompareFunction.Always; + uid.DepthWriteEnabled = depthTest.TestEnable && depthTest.WriteEnable; + + SignalDirty(DirtyFlags.DepthStencil); + } + + public readonly void UpdateDepthClamp(bool clamp) + { + _currentState.DepthClipMode = clamp ? MTLDepthClipMode.Clamp : MTLDepthClipMode.Clip; + + // Inline update + if (_pipeline.Encoders.TryGetRenderEncoder(out MTLRenderCommandEncoder renderCommandEncoder)) + { + SetDepthClamp(renderCommandEncoder); + return; + } + + SignalDirty(DirtyFlags.DepthClamp); + } + + public readonly void UpdateDepthBias(float depthBias, float slopeScale, float clamp) + { + _currentState.DepthBias = depthBias; + _currentState.SlopeScale = slopeScale; + _currentState.Clamp = clamp; + + // Inline update + if (_pipeline.Encoders.TryGetRenderEncoder(out MTLRenderCommandEncoder renderCommandEncoder)) + { + SetDepthBias(renderCommandEncoder); + return; + } + + SignalDirty(DirtyFlags.DepthBias); + } + + public readonly void UpdateLogicOpState(bool enable, LogicalOp op) + { + _currentState.Pipeline.LogicOpEnable = enable; + _currentState.Pipeline.LogicOp = op.Convert(); + + SignalDirty(DirtyFlags.RenderPipeline); + } + + public readonly void UpdateMultisampleState(MultisampleDescriptor multisample) + { + _currentState.Pipeline.AlphaToCoverageEnable = multisample.AlphaToCoverageEnable; + _currentState.Pipeline.AlphaToOneEnable = multisample.AlphaToOneEnable; + + SignalDirty(DirtyFlags.RenderPipeline); + } + + public void UpdateScissors(ReadOnlySpan> regions) + { + for (int i = 0; i < regions.Length; i++) + { + var region = regions[i]; + + _currentState.Scissors[i] = new MTLScissorRect + { + height = (ulong)region.Height, + width = (ulong)region.Width, + x = (ulong)region.X, + y = (ulong)region.Y + }; + } + + // Inline update + if (_pipeline.Encoders.TryGetRenderEncoder(out MTLRenderCommandEncoder renderCommandEncoder)) + { + SetScissors(renderCommandEncoder); + return; + } + + SignalDirty(DirtyFlags.Scissors); + } + + public void UpdateViewports(ReadOnlySpan viewports) + { + static float Clamp(float value) + { + return Math.Clamp(value, 0f, 1f); + } + + for (int i = 0; i < viewports.Length; i++) + { + var viewport = viewports[i]; + // Y coordinate is inverted + _currentState.Viewports[i] = new MTLViewport + { + originX = viewport.Region.X, + originY = viewport.Region.Y + viewport.Region.Height, + width = viewport.Region.Width, + height = -viewport.Region.Height, + znear = Clamp(viewport.DepthNear), + zfar = Clamp(viewport.DepthFar) + }; + } + + // Inline update + if (_pipeline.Encoders.TryGetRenderEncoder(out MTLRenderCommandEncoder renderCommandEncoder)) + { + SetViewports(renderCommandEncoder); + return; + } + + SignalDirty(DirtyFlags.Viewports); + } + + public readonly void UpdateVertexBuffers(ReadOnlySpan vertexBuffers) + { + for (int i = 0; i < Constants.MaxVertexBuffers; i++) + { + if (i < vertexBuffers.Length) + { + var vertexBuffer = vertexBuffers[i]; + + _currentState.VertexBuffers[i] = new VertexBufferState( + vertexBuffer.Buffer.Handle, + vertexBuffer.Buffer.Offset, + vertexBuffer.Buffer.Size, + vertexBuffer.Divisor, + vertexBuffer.Stride); + } + else + { + _currentState.VertexBuffers[i] = VertexBufferState.Null; + } + } + + // Update the buffers on the pipeline + UpdatePipelineVertexState(_currentState.VertexBuffers, _currentState.VertexAttribs); + + SignalDirty(DirtyFlags.RenderPipeline); + } + + public readonly void UpdateUniformBuffers(ReadOnlySpan buffers) + { + foreach (BufferAssignment assignment in buffers) + { + var buffer = assignment.Range; + int index = assignment.Binding; + + Auto mtlBuffer = buffer.Handle == BufferHandle.Null + ? null + : _bufferManager.GetBuffer(buffer.Handle, buffer.Write); + + _currentState.UniformBufferRefs[index] = new BufferRef(mtlBuffer, ref buffer); + } + + SignalDirty(DirtyFlags.Uniforms); + } + + public readonly void UpdateStorageBuffers(ReadOnlySpan buffers) + { + foreach (BufferAssignment assignment in buffers) + { + var buffer = assignment.Range; + int index = assignment.Binding; + + Auto mtlBuffer = buffer.Handle == BufferHandle.Null + ? null + : _bufferManager.GetBuffer(buffer.Handle, buffer.Write); + + _currentState.StorageBufferRefs[index] = new BufferRef(mtlBuffer, ref buffer); + } + + SignalDirty(DirtyFlags.Storages); + } + + public readonly void UpdateStorageBuffers(int first, ReadOnlySpan> buffers) + { + for (int i = 0; i < buffers.Length; i++) + { + var mtlBuffer = buffers[i]; + int index = first + i; + + _currentState.StorageBufferRefs[index] = new BufferRef(mtlBuffer); + } + + SignalDirty(DirtyFlags.Storages); + } + + public void UpdateCullMode(bool enable, Face face) + { + var dirtyScissor = (face == Face.FrontAndBack) != _currentState.CullBoth; + + _currentState.CullMode = enable ? face.Convert() : MTLCullMode.None; + _currentState.CullBoth = face == Face.FrontAndBack; + + // Inline update + if (_pipeline.Encoders.TryGetRenderEncoder(out MTLRenderCommandEncoder renderCommandEncoder)) + { + SetCullMode(renderCommandEncoder); + SetScissors(renderCommandEncoder); + return; + } + + // Mark dirty + SignalDirty(DirtyFlags.CullMode); + + if (dirtyScissor) + { + SignalDirty(DirtyFlags.Scissors); + } + } + + public readonly void UpdateFrontFace(FrontFace frontFace) + { + _currentState.Winding = frontFace.Convert(); + + // Inline update + if (_pipeline.Encoders.TryGetRenderEncoder(out MTLRenderCommandEncoder renderCommandEncoder)) + { + SetFrontFace(renderCommandEncoder); + return; + } + + SignalDirty(DirtyFlags.FrontFace); + } + + private readonly void UpdateStencilRefValue(int frontRef, int backRef) + { + _currentState.FrontRefValue = frontRef; + _currentState.BackRefValue = backRef; + + // Inline update + if (_pipeline.Encoders.TryGetRenderEncoder(out MTLRenderCommandEncoder renderCommandEncoder)) + { + SetStencilRefValue(renderCommandEncoder); + } + + SignalDirty(DirtyFlags.StencilRef); + } + + public readonly void UpdateTextureAndSampler(ShaderStage stage, int binding, TextureBase texture, SamplerHolder samplerHolder) + { + if (texture != null) + { + _currentState.TextureRefs[binding] = new(stage, texture, samplerHolder?.GetSampler()); + } + else + { + _currentState.TextureRefs[binding] = default; + } + + SignalDirty(DirtyFlags.Textures); + } + + public readonly void UpdateImage(ShaderStage stage, int binding, TextureBase image) + { + if (image is Texture view) + { + _currentState.ImageRefs[binding] = new(stage, view); + } + else + { + _currentState.ImageRefs[binding] = default; + } + + SignalDirty(DirtyFlags.Images); + } + + public readonly void UpdateTextureArray(ShaderStage stage, int binding, TextureArray array) + { + ref EncoderState.ArrayRef arrayRef = ref GetArrayRef(ref _currentState.TextureArrayRefs, binding, ArrayGrowthSize); + + if (arrayRef.Stage != stage || arrayRef.Array != array) + { + arrayRef = new EncoderState.ArrayRef(stage, array); + + SignalDirty(DirtyFlags.Textures); + } + } + + public readonly void UpdateTextureArraySeparate(ShaderStage stage, int setIndex, TextureArray array) + { + ref EncoderState.ArrayRef arrayRef = ref GetArrayRef(ref _currentState.TextureArrayExtraRefs, setIndex - MetalRenderer.TotalSets); + + if (arrayRef.Stage != stage || arrayRef.Array != array) + { + arrayRef = new EncoderState.ArrayRef(stage, array); + + SignalDirty(DirtyFlags.Textures); + } + } + + public readonly void UpdateImageArray(ShaderStage stage, int binding, ImageArray array) + { + ref EncoderState.ArrayRef arrayRef = ref GetArrayRef(ref _currentState.ImageArrayRefs, binding, ArrayGrowthSize); + + if (arrayRef.Stage != stage || arrayRef.Array != array) + { + arrayRef = new EncoderState.ArrayRef(stage, array); + + SignalDirty(DirtyFlags.Images); + } + } + + public readonly void UpdateImageArraySeparate(ShaderStage stage, int setIndex, ImageArray array) + { + ref EncoderState.ArrayRef arrayRef = ref GetArrayRef(ref _currentState.ImageArrayExtraRefs, setIndex - MetalRenderer.TotalSets); + + if (arrayRef.Stage != stage || arrayRef.Array != array) + { + arrayRef = new EncoderState.ArrayRef(stage, array); + + SignalDirty(DirtyFlags.Images); + } + } + + private static ref EncoderState.ArrayRef GetArrayRef(ref EncoderState.ArrayRef[] array, int index, int growthSize = 1) + { + ArgumentOutOfRangeException.ThrowIfNegative(index); + + if (array.Length <= index) + { + Array.Resize(ref array, index + growthSize); + } + + return ref array[index]; + } + + private readonly void SetDepthStencilState(MTLRenderCommandEncoder renderCommandEncoder) + { + if (DepthStencil != null) + { + MTLDepthStencilState state = _depthStencilCache.GetOrCreate(_currentState.DepthStencilUid); + + renderCommandEncoder.SetDepthStencilState(state); + } + else + { + renderCommandEncoder.SetDepthStencilState(_defaultState); + } + } + + private readonly void SetDepthClamp(MTLRenderCommandEncoder renderCommandEncoder) + { + renderCommandEncoder.SetDepthClipMode(_currentState.DepthClipMode); + } + + private readonly void SetDepthBias(MTLRenderCommandEncoder renderCommandEncoder) + { + renderCommandEncoder.SetDepthBias(_currentState.DepthBias, _currentState.SlopeScale, _currentState.Clamp); + } + + private unsafe void SetScissors(MTLRenderCommandEncoder renderCommandEncoder) + { + var isTriangles = (_currentState.Topology == PrimitiveTopology.Triangles) || + (_currentState.Topology == PrimitiveTopology.TriangleStrip); + + if (_currentState.CullBoth && isTriangles) + { + renderCommandEncoder.SetScissorRect(new MTLScissorRect { x = 0, y = 0, width = 0, height = 0 }); + } + else + { + if (_currentState.Scissors.Length > 0) + { + fixed (MTLScissorRect* pMtlScissors = _currentState.Scissors) + { + renderCommandEncoder.SetScissorRects((IntPtr)pMtlScissors, (ulong)_currentState.Scissors.Length); + } + } + } + } + + private readonly unsafe void SetViewports(MTLRenderCommandEncoder renderCommandEncoder) + { + if (_currentState.Viewports.Length > 0) + { + fixed (MTLViewport* pMtlViewports = _currentState.Viewports) + { + renderCommandEncoder.SetViewports((IntPtr)pMtlViewports, (ulong)_currentState.Viewports.Length); + } + } + } + + private readonly void UpdatePipelineVertexState(VertexBufferState[] bufferDescriptors, VertexAttribDescriptor[] attribDescriptors) + { + ref PipelineState pipeline = ref _currentState.Pipeline; + uint indexMask = 0; + + for (int i = 0; i < attribDescriptors.Length; i++) + { + ref var attrib = ref pipeline.Internal.VertexAttributes[i]; + + if (attribDescriptors[i].IsZero) + { + attrib.Format = attribDescriptors[i].Format.Convert(); + indexMask |= 1u << (int)Constants.ZeroBufferIndex; + attrib.BufferIndex = Constants.ZeroBufferIndex; + attrib.Offset = 0; + } + else + { + attrib.Format = attribDescriptors[i].Format.Convert(); + indexMask |= 1u << attribDescriptors[i].BufferIndex; + attrib.BufferIndex = (ulong)attribDescriptors[i].BufferIndex; + attrib.Offset = (ulong)attribDescriptors[i].Offset; + } + } + + for (int i = 0; i < bufferDescriptors.Length; i++) + { + ref var layout = ref pipeline.Internal.VertexBindings[i]; + + if ((indexMask & (1u << i)) != 0) + { + layout.Stride = (uint)bufferDescriptors[i].Stride; + + if (layout.Stride == 0) + { + layout.Stride = 1; + layout.StepFunction = MTLVertexStepFunction.Constant; + layout.StepRate = 0; + } + else + { + if (bufferDescriptors[i].Divisor > 0) + { + layout.StepFunction = MTLVertexStepFunction.PerInstance; + layout.StepRate = (uint)bufferDescriptors[i].Divisor; + } + else + { + layout.StepFunction = MTLVertexStepFunction.PerVertex; + layout.StepRate = 1; + } + } + } + else + { + layout = new(); + } + } + + ref var zeroBufLayout = ref pipeline.Internal.VertexBindings[(int)Constants.ZeroBufferIndex]; + + // Zero buffer + if ((indexMask & (1u << (int)Constants.ZeroBufferIndex)) != 0) + { + zeroBufLayout.Stride = 1; + zeroBufLayout.StepFunction = MTLVertexStepFunction.Constant; + zeroBufLayout.StepRate = 0; + } + else + { + zeroBufLayout = new(); + } + + pipeline.VertexAttributeDescriptionsCount = (uint)attribDescriptors.Length; + pipeline.VertexBindingDescriptionsCount = Constants.ZeroBufferIndex + 1; // TODO: move this out? + } + + private readonly void SetVertexBuffers(VertexBufferState[] bufferStates, ref readonly RenderEncoderBindings bindings) + { + for (int i = 0; i < bufferStates.Length; i++) + { + (MTLBuffer mtlBuffer, int offset) = bufferStates[i].GetVertexBuffer(_bufferManager, _pipeline.Cbs); + + if (mtlBuffer.NativePtr != IntPtr.Zero) + { + bindings.VertexBuffers.Add(new BufferResource(mtlBuffer, (ulong)offset, (ulong)i)); + } + } + + Auto autoZeroBuffer = _zeroBuffer == BufferHandle.Null + ? null + : _bufferManager.GetBuffer(_zeroBuffer, false); + + if (autoZeroBuffer == null) + { + return; + } + + var zeroMtlBuffer = autoZeroBuffer.Get(_pipeline.Cbs).Value; + bindings.VertexBuffers.Add(new BufferResource(zeroMtlBuffer, 0, Constants.ZeroBufferIndex)); + } + + private readonly (ulong gpuAddress, IntPtr nativePtr) AddressForBuffer(ref BufferRef buffer) + { + ulong gpuAddress = 0; + IntPtr nativePtr = IntPtr.Zero; + + var range = buffer.Range; + var autoBuffer = buffer.Buffer; + + if (autoBuffer != null) + { + var offset = 0; + MTLBuffer mtlBuffer; + + if (range.HasValue) + { + offset = range.Value.Offset; + mtlBuffer = autoBuffer.Get(_pipeline.Cbs, offset, range.Value.Size, range.Value.Write).Value; + } + else + { + mtlBuffer = autoBuffer.Get(_pipeline.Cbs).Value; + } + + gpuAddress = mtlBuffer.GpuAddress + (ulong)offset; + nativePtr = mtlBuffer.NativePtr; + } + + return (gpuAddress, nativePtr); + } + + private readonly (ulong gpuAddress, IntPtr nativePtr) AddressForTexture(ref TextureRef texture) + { + var storage = texture.Storage; + + ulong gpuAddress = 0; + IntPtr nativePtr = IntPtr.Zero; + + if (storage != null) + { + if (storage is TextureBuffer textureBuffer) + { + textureBuffer.RebuildStorage(false); + } + + var mtlTexture = storage.GetHandle(); + + gpuAddress = mtlTexture.GpuResourceID._impl; + nativePtr = mtlTexture.NativePtr; + } + + return (gpuAddress, nativePtr); + } + + private readonly (ulong gpuAddress, IntPtr nativePtr) AddressForImage(ref ImageRef image) + { + var storage = image.Storage; + + ulong gpuAddress = 0; + IntPtr nativePtr = IntPtr.Zero; + + if (storage != null) + { + var mtlTexture = storage.GetHandle(); + + gpuAddress = mtlTexture.GpuResourceID._impl; + nativePtr = mtlTexture.NativePtr; + } + + return (gpuAddress, nativePtr); + } + + private readonly (ulong gpuAddress, IntPtr nativePtr) AddressForTextureBuffer(ref TextureBuffer bufferTexture) + { + ulong gpuAddress = 0; + IntPtr nativePtr = IntPtr.Zero; + + if (bufferTexture != null) + { + bufferTexture.RebuildStorage(false); + + var mtlTexture = bufferTexture.GetHandle(); + + gpuAddress = mtlTexture.GpuResourceID._impl; + nativePtr = mtlTexture.NativePtr; + } + + return (gpuAddress, nativePtr); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AddResource(IntPtr resourcePointer, MTLResourceUsage usage, MTLRenderStages stages, ref readonly RenderEncoderBindings bindings) + { + if (resourcePointer != IntPtr.Zero) + { + bindings.Resources.Add(new Resource(new MTLResource(resourcePointer), usage, stages)); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void AddResource(IntPtr resourcePointer, MTLResourceUsage usage, ref readonly ComputeEncoderBindings bindings) + { + if (resourcePointer != IntPtr.Zero) + { + bindings.Resources.Add(new Resource(new MTLResource(resourcePointer), usage, 0)); + } + } + + private readonly void UpdateAndBind(Program program, uint setIndex, ref readonly RenderEncoderBindings bindings) + { + var bindingSegments = program.BindingSegments[setIndex]; + + if (bindingSegments.Length == 0) + { + return; + } + + ScopedTemporaryBuffer vertArgBuffer = default; + ScopedTemporaryBuffer fragArgBuffer = default; + + if (program.ArgumentBufferSizes[setIndex] > 0) + { + vertArgBuffer = _bufferManager.ReserveOrCreate(_pipeline.Cbs, program.ArgumentBufferSizes[setIndex] * sizeof(ulong)); + } + + if (program.FragArgumentBufferSizes[setIndex] > 0) + { + fragArgBuffer = _bufferManager.ReserveOrCreate(_pipeline.Cbs, program.FragArgumentBufferSizes[setIndex] * sizeof(ulong)); + } + + Span vertResourceIds = stackalloc ulong[program.ArgumentBufferSizes[setIndex]]; + Span fragResourceIds = stackalloc ulong[program.FragArgumentBufferSizes[setIndex]]; + + var vertResourceIdIndex = 0; + var fragResourceIdIndex = 0; + + foreach (ResourceBindingSegment segment in bindingSegments) + { + int binding = segment.Binding; + int count = segment.Count; + + switch (setIndex) + { + case Constants.ConstantBuffersSetIndex: + for (int i = 0; i < count; i++) + { + int index = binding + i; + + ref BufferRef buffer = ref _currentState.UniformBufferRefs[index]; + var (gpuAddress, nativePtr) = AddressForBuffer(ref buffer); + + MTLRenderStages renderStages = 0; + + if ((segment.Stages & ResourceStages.Vertex) != 0) + { + vertResourceIds[vertResourceIdIndex] = gpuAddress; + vertResourceIdIndex++; + + renderStages |= MTLRenderStages.RenderStageVertex; + } + + if ((segment.Stages & ResourceStages.Fragment) != 0) + { + fragResourceIds[fragResourceIdIndex] = gpuAddress; + fragResourceIdIndex++; + + renderStages |= MTLRenderStages.RenderStageFragment; + } + + AddResource(nativePtr, MTLResourceUsage.Read, renderStages, in bindings); + } + break; + case Constants.StorageBuffersSetIndex: + for (int i = 0; i < count; i++) + { + int index = binding + i; + + ref BufferRef buffer = ref _currentState.StorageBufferRefs[index]; + var (gpuAddress, nativePtr) = AddressForBuffer(ref buffer); + + MTLRenderStages renderStages = 0; + + if ((segment.Stages & ResourceStages.Vertex) != 0) + { + vertResourceIds[vertResourceIdIndex] = gpuAddress; + vertResourceIdIndex++; + + renderStages |= MTLRenderStages.RenderStageVertex; + } + + if ((segment.Stages & ResourceStages.Fragment) != 0) + { + fragResourceIds[fragResourceIdIndex] = gpuAddress; + fragResourceIdIndex++; + + renderStages |= MTLRenderStages.RenderStageFragment; + } + + AddResource(nativePtr, MTLResourceUsage.Read, renderStages, in bindings); + } + break; + case Constants.TexturesSetIndex: + if (!segment.IsArray) + { + for (int i = 0; i < count; i++) + { + int index = binding + i; + + ref var texture = ref _currentState.TextureRefs[index]; + var (gpuAddress, nativePtr) = AddressForTexture(ref texture); + + MTLRenderStages renderStages = 0; + + if ((segment.Stages & ResourceStages.Vertex) != 0) + { + vertResourceIds[vertResourceIdIndex] = gpuAddress; + vertResourceIdIndex++; + + if (texture.Sampler != null) + { + vertResourceIds[vertResourceIdIndex] = texture.Sampler.Get(_pipeline.Cbs).Value.GpuResourceID._impl; + vertResourceIdIndex++; + } + + renderStages |= MTLRenderStages.RenderStageVertex; + } + + if ((segment.Stages & ResourceStages.Fragment) != 0) + { + fragResourceIds[fragResourceIdIndex] = gpuAddress; + fragResourceIdIndex++; + + if (texture.Sampler != null) + { + fragResourceIds[fragResourceIdIndex] = texture.Sampler.Get(_pipeline.Cbs).Value.GpuResourceID._impl; + fragResourceIdIndex++; + } + + renderStages |= MTLRenderStages.RenderStageFragment; + } + + AddResource(nativePtr, MTLResourceUsage.Read, renderStages, in bindings); + } + } + else + { + var textureArray = _currentState.TextureArrayRefs[binding].Array; + + if (segment.Type != ResourceType.BufferTexture) + { + var textures = textureArray.GetTextureRefs(); + var samplers = new Auto[textures.Length]; + + for (int i = 0; i < textures.Length; i++) + { + TextureRef texture = textures[i]; + var (gpuAddress, nativePtr) = AddressForTexture(ref texture); + + samplers[i] = texture.Sampler; + + MTLRenderStages renderStages = 0; + + if ((segment.Stages & ResourceStages.Vertex) != 0) + { + vertResourceIds[vertResourceIdIndex] = gpuAddress; + vertResourceIdIndex++; + + renderStages |= MTLRenderStages.RenderStageVertex; + } + + if ((segment.Stages & ResourceStages.Fragment) != 0) + { + fragResourceIds[fragResourceIdIndex] = gpuAddress; + fragResourceIdIndex++; + + renderStages |= MTLRenderStages.RenderStageFragment; + } + + AddResource(nativePtr, MTLResourceUsage.Read, renderStages, in bindings); + } + + foreach (var sampler in samplers) + { + ulong gpuAddress = 0; + + if (sampler != null) + { + gpuAddress = sampler.Get(_pipeline.Cbs).Value.GpuResourceID._impl; + } + + if ((segment.Stages & ResourceStages.Vertex) != 0) + { + vertResourceIds[vertResourceIdIndex] = gpuAddress; + vertResourceIdIndex++; + } + + if ((segment.Stages & ResourceStages.Fragment) != 0) + { + fragResourceIds[fragResourceIdIndex] = gpuAddress; + fragResourceIdIndex++; + } + } + } + else + { + var bufferTextures = textureArray.GetBufferTextureRefs(); + + for (int i = 0; i < bufferTextures.Length; i++) + { + TextureBuffer bufferTexture = bufferTextures[i]; + var (gpuAddress, nativePtr) = AddressForTextureBuffer(ref bufferTexture); + + MTLRenderStages renderStages = 0; + + if ((segment.Stages & ResourceStages.Vertex) != 0) + { + vertResourceIds[vertResourceIdIndex] = gpuAddress; + vertResourceIdIndex++; + + renderStages |= MTLRenderStages.RenderStageVertex; + } + + if ((segment.Stages & ResourceStages.Fragment) != 0) + { + fragResourceIds[fragResourceIdIndex] = gpuAddress; + fragResourceIdIndex++; + + renderStages |= MTLRenderStages.RenderStageFragment; + } + + AddResource(nativePtr, MTLResourceUsage.Read, renderStages, in bindings); + } + } + } + break; + case Constants.ImagesSetIndex: + if (!segment.IsArray) + { + for (int i = 0; i < count; i++) + { + int index = binding + i; + + ref var image = ref _currentState.ImageRefs[index]; + var (gpuAddress, nativePtr) = AddressForImage(ref image); + + MTLRenderStages renderStages = 0; + + if ((segment.Stages & ResourceStages.Vertex) != 0) + { + vertResourceIds[vertResourceIdIndex] = gpuAddress; + vertResourceIdIndex++; + renderStages |= MTLRenderStages.RenderStageVertex; + } + + if ((segment.Stages & ResourceStages.Fragment) != 0) + { + fragResourceIds[fragResourceIdIndex] = gpuAddress; + fragResourceIdIndex++; + renderStages |= MTLRenderStages.RenderStageFragment; + } + + AddResource(nativePtr, MTLResourceUsage.Read | MTLResourceUsage.Write, renderStages, in bindings); + } + } + else + { + var imageArray = _currentState.ImageArrayRefs[binding].Array; + + if (segment.Type != ResourceType.BufferImage) + { + var images = imageArray.GetTextureRefs(); + + for (int i = 0; i < images.Length; i++) + { + TextureRef image = images[i]; + var (gpuAddress, nativePtr) = AddressForTexture(ref image); + + MTLRenderStages renderStages = 0; + + if ((segment.Stages & ResourceStages.Vertex) != 0) + { + vertResourceIds[vertResourceIdIndex] = gpuAddress; + vertResourceIdIndex++; + renderStages |= MTLRenderStages.RenderStageVertex; + } + + if ((segment.Stages & ResourceStages.Fragment) != 0) + { + fragResourceIds[fragResourceIdIndex] = gpuAddress; + fragResourceIdIndex++; + renderStages |= MTLRenderStages.RenderStageFragment; + } + + AddResource(nativePtr, MTLResourceUsage.Read | MTLResourceUsage.Write, renderStages, in bindings); + } + } + else + { + var bufferImages = imageArray.GetBufferTextureRefs(); + + for (int i = 0; i < bufferImages.Length; i++) + { + TextureBuffer image = bufferImages[i]; + var (gpuAddress, nativePtr) = AddressForTextureBuffer(ref image); + + MTLRenderStages renderStages = 0; + + if ((segment.Stages & ResourceStages.Vertex) != 0) + { + vertResourceIds[vertResourceIdIndex] = gpuAddress; + vertResourceIdIndex++; + renderStages |= MTLRenderStages.RenderStageVertex; + } + + if ((segment.Stages & ResourceStages.Fragment) != 0) + { + fragResourceIds[fragResourceIdIndex] = gpuAddress; + fragResourceIdIndex++; + renderStages |= MTLRenderStages.RenderStageFragment; + } + + AddResource(nativePtr, MTLResourceUsage.Read | MTLResourceUsage.Write, renderStages, in bindings); + } + } + } + break; + } + } + + if (program.ArgumentBufferSizes[setIndex] > 0) + { + vertArgBuffer.Holder.SetDataUnchecked(vertArgBuffer.Offset, MemoryMarshal.AsBytes(vertResourceIds)); + var mtlVertArgBuffer = _bufferManager.GetBuffer(vertArgBuffer.Handle, false).Get(_pipeline.Cbs).Value; + bindings.VertexBuffers.Add(new BufferResource(mtlVertArgBuffer, (uint)vertArgBuffer.Range.Offset, SetIndexToBindingIndex(setIndex))); + } + + if (program.FragArgumentBufferSizes[setIndex] > 0) + { + fragArgBuffer.Holder.SetDataUnchecked(fragArgBuffer.Offset, MemoryMarshal.AsBytes(fragResourceIds)); + var mtlFragArgBuffer = _bufferManager.GetBuffer(fragArgBuffer.Handle, false).Get(_pipeline.Cbs).Value; + bindings.FragmentBuffers.Add(new BufferResource(mtlFragArgBuffer, (uint)fragArgBuffer.Range.Offset, SetIndexToBindingIndex(setIndex))); + } + } + + private readonly void UpdateAndBind(Program program, uint setIndex, ref readonly ComputeEncoderBindings bindings) + { + var bindingSegments = program.BindingSegments[setIndex]; + + if (bindingSegments.Length == 0) + { + return; + } + + ScopedTemporaryBuffer argBuffer = default; + + if (program.ArgumentBufferSizes[setIndex] > 0) + { + argBuffer = _bufferManager.ReserveOrCreate(_pipeline.Cbs, program.ArgumentBufferSizes[setIndex] * sizeof(ulong)); + } + + Span resourceIds = stackalloc ulong[program.ArgumentBufferSizes[setIndex]]; + var resourceIdIndex = 0; + + foreach (ResourceBindingSegment segment in bindingSegments) + { + int binding = segment.Binding; + int count = segment.Count; + + switch (setIndex) + { + case Constants.ConstantBuffersSetIndex: + for (int i = 0; i < count; i++) + { + int index = binding + i; + + ref BufferRef buffer = ref _currentState.UniformBufferRefs[index]; + var (gpuAddress, nativePtr) = AddressForBuffer(ref buffer); + + if ((segment.Stages & ResourceStages.Compute) != 0) + { + AddResource(nativePtr, MTLResourceUsage.Read, in bindings); + bindings.Resources.Add(new Resource(new MTLResource(nativePtr), MTLResourceUsage.Read, 0)); + resourceIds[resourceIdIndex] = gpuAddress; + resourceIdIndex++; + } + } + break; + case Constants.StorageBuffersSetIndex: + for (int i = 0; i < count; i++) + { + int index = binding + i; + + ref BufferRef buffer = ref _currentState.StorageBufferRefs[index]; + var (gpuAddress, nativePtr) = AddressForBuffer(ref buffer); + + if ((segment.Stages & ResourceStages.Compute) != 0) + { + AddResource(nativePtr, MTLResourceUsage.Read | MTLResourceUsage.Write, in bindings); + resourceIds[resourceIdIndex] = gpuAddress; + resourceIdIndex++; + } + } + break; + case Constants.TexturesSetIndex: + if (!segment.IsArray) + { + for (int i = 0; i < count; i++) + { + int index = binding + i; + + ref var texture = ref _currentState.TextureRefs[index]; + var (gpuAddress, nativePtr) = AddressForTexture(ref texture); + + if ((segment.Stages & ResourceStages.Compute) != 0) + { + AddResource(nativePtr, MTLResourceUsage.Read, in bindings); + resourceIds[resourceIdIndex] = gpuAddress; + resourceIdIndex++; + + if (texture.Sampler != null) + { + resourceIds[resourceIdIndex] = texture.Sampler.Get(_pipeline.Cbs).Value.GpuResourceID._impl; + resourceIdIndex++; + } + } + } + } + else + { + var textureArray = _currentState.TextureArrayRefs[binding].Array; + + if (segment.Type != ResourceType.BufferTexture) + { + var textures = textureArray.GetTextureRefs(); + var samplers = new Auto[textures.Length]; + + for (int i = 0; i < textures.Length; i++) + { + TextureRef texture = textures[i]; + var (gpuAddress, nativePtr) = AddressForTexture(ref texture); + + if ((segment.Stages & ResourceStages.Compute) != 0) + { + AddResource(nativePtr, MTLResourceUsage.Read, in bindings); + resourceIds[resourceIdIndex] = gpuAddress; + resourceIdIndex++; + + samplers[i] = texture.Sampler; + } + } + + foreach (var sampler in samplers) + { + if (sampler != null) + { + resourceIds[resourceIdIndex] = sampler.Get(_pipeline.Cbs).Value.GpuResourceID._impl; + resourceIdIndex++; + } + } + } + else + { + var bufferTextures = textureArray.GetBufferTextureRefs(); + + for (int i = 0; i < bufferTextures.Length; i++) + { + TextureBuffer bufferTexture = bufferTextures[i]; + var (gpuAddress, nativePtr) = AddressForTextureBuffer(ref bufferTexture); + + if ((segment.Stages & ResourceStages.Compute) != 0) + { + AddResource(nativePtr, MTLResourceUsage.Read, in bindings); + resourceIds[resourceIdIndex] = gpuAddress; + resourceIdIndex++; + } + } + } + } + break; + case Constants.ImagesSetIndex: + if (!segment.IsArray) + { + for (int i = 0; i < count; i++) + { + int index = binding + i; + + ref var image = ref _currentState.ImageRefs[index]; + var (gpuAddress, nativePtr) = AddressForImage(ref image); + + if ((segment.Stages & ResourceStages.Compute) != 0) + { + AddResource(nativePtr, MTLResourceUsage.Read | MTLResourceUsage.Write, in bindings); + resourceIds[resourceIdIndex] = gpuAddress; + resourceIdIndex++; + } + } + } + else + { + var imageArray = _currentState.ImageArrayRefs[binding].Array; + + if (segment.Type != ResourceType.BufferImage) + { + var images = imageArray.GetTextureRefs(); + + for (int i = 0; i < images.Length; i++) + { + TextureRef image = images[i]; + var (gpuAddress, nativePtr) = AddressForTexture(ref image); + + if ((segment.Stages & ResourceStages.Compute) != 0) + { + AddResource(nativePtr, MTLResourceUsage.Read | MTLResourceUsage.Write, in bindings); + resourceIds[resourceIdIndex] = gpuAddress; + resourceIdIndex++; + } + } + } + else + { + var bufferImages = imageArray.GetBufferTextureRefs(); + + for (int i = 0; i < bufferImages.Length; i++) + { + TextureBuffer image = bufferImages[i]; + var (gpuAddress, nativePtr) = AddressForTextureBuffer(ref image); + + if ((segment.Stages & ResourceStages.Compute) != 0) + { + AddResource(nativePtr, MTLResourceUsage.Read | MTLResourceUsage.Write, in bindings); + resourceIds[resourceIdIndex] = gpuAddress; + resourceIdIndex++; + } + } + } + } + break; + } + } + + if (program.ArgumentBufferSizes[setIndex] > 0) + { + argBuffer.Holder.SetDataUnchecked(argBuffer.Offset, MemoryMarshal.AsBytes(resourceIds)); + var mtlArgBuffer = _bufferManager.GetBuffer(argBuffer.Handle, false).Get(_pipeline.Cbs).Value; + bindings.Buffers.Add(new BufferResource(mtlArgBuffer, (uint)argBuffer.Range.Offset, SetIndexToBindingIndex(setIndex))); + } + } + + private static uint SetIndexToBindingIndex(uint setIndex) + { + return setIndex switch + { + Constants.ConstantBuffersSetIndex => Constants.ConstantBuffersIndex, + Constants.StorageBuffersSetIndex => Constants.StorageBuffersIndex, + Constants.TexturesSetIndex => Constants.TexturesIndex, + Constants.ImagesSetIndex => Constants.ImagesIndex, + }; + } + + private readonly void SetCullMode(MTLRenderCommandEncoder renderCommandEncoder) + { + renderCommandEncoder.SetCullMode(_currentState.CullMode); + } + + private readonly void SetFrontFace(MTLRenderCommandEncoder renderCommandEncoder) + { + renderCommandEncoder.SetFrontFacingWinding(_currentState.Winding); + } + + private readonly void SetStencilRefValue(MTLRenderCommandEncoder renderCommandEncoder) + { + renderCommandEncoder.SetStencilReferenceValues((uint)_currentState.FrontRefValue, (uint)_currentState.BackRefValue); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/EnumConversion.cs b/src/Ryujinx.Graphics.Metal/EnumConversion.cs new file mode 100644 index 000000000..e498546e8 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/EnumConversion.cs @@ -0,0 +1,293 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.GAL; +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + static class EnumConversion + { + public static MTLSamplerAddressMode Convert(this AddressMode mode) + { + return mode switch + { + AddressMode.Clamp => MTLSamplerAddressMode.ClampToEdge, // TODO: Should be clamp. + AddressMode.Repeat => MTLSamplerAddressMode.Repeat, + AddressMode.MirrorClamp => MTLSamplerAddressMode.MirrorClampToEdge, // TODO: Should be mirror clamp. + AddressMode.MirroredRepeat => MTLSamplerAddressMode.MirrorRepeat, + AddressMode.ClampToBorder => MTLSamplerAddressMode.ClampToBorderColor, + AddressMode.ClampToEdge => MTLSamplerAddressMode.ClampToEdge, + AddressMode.MirrorClampToEdge => MTLSamplerAddressMode.MirrorClampToEdge, + AddressMode.MirrorClampToBorder => MTLSamplerAddressMode.ClampToBorderColor, // TODO: Should be mirror clamp to border. + _ => LogInvalidAndReturn(mode, nameof(AddressMode), MTLSamplerAddressMode.ClampToEdge) // TODO: Should be clamp. + }; + } + + public static MTLBlendFactor Convert(this BlendFactor factor) + { + return factor switch + { + BlendFactor.Zero or BlendFactor.ZeroGl => MTLBlendFactor.Zero, + BlendFactor.One or BlendFactor.OneGl => MTLBlendFactor.One, + BlendFactor.SrcColor or BlendFactor.SrcColorGl => MTLBlendFactor.SourceColor, + BlendFactor.OneMinusSrcColor or BlendFactor.OneMinusSrcColorGl => MTLBlendFactor.OneMinusSourceColor, + BlendFactor.SrcAlpha or BlendFactor.SrcAlphaGl => MTLBlendFactor.SourceAlpha, + BlendFactor.OneMinusSrcAlpha or BlendFactor.OneMinusSrcAlphaGl => MTLBlendFactor.OneMinusSourceAlpha, + BlendFactor.DstAlpha or BlendFactor.DstAlphaGl => MTLBlendFactor.DestinationAlpha, + BlendFactor.OneMinusDstAlpha or BlendFactor.OneMinusDstAlphaGl => MTLBlendFactor.OneMinusDestinationAlpha, + BlendFactor.DstColor or BlendFactor.DstColorGl => MTLBlendFactor.DestinationColor, + BlendFactor.OneMinusDstColor or BlendFactor.OneMinusDstColorGl => MTLBlendFactor.OneMinusDestinationColor, + BlendFactor.SrcAlphaSaturate or BlendFactor.SrcAlphaSaturateGl => MTLBlendFactor.SourceAlphaSaturated, + BlendFactor.Src1Color or BlendFactor.Src1ColorGl => MTLBlendFactor.Source1Color, + BlendFactor.OneMinusSrc1Color or BlendFactor.OneMinusSrc1ColorGl => MTLBlendFactor.OneMinusSource1Color, + BlendFactor.Src1Alpha or BlendFactor.Src1AlphaGl => MTLBlendFactor.Source1Alpha, + BlendFactor.OneMinusSrc1Alpha or BlendFactor.OneMinusSrc1AlphaGl => MTLBlendFactor.OneMinusSource1Alpha, + BlendFactor.ConstantColor => MTLBlendFactor.BlendColor, + BlendFactor.OneMinusConstantColor => MTLBlendFactor.OneMinusBlendColor, + BlendFactor.ConstantAlpha => MTLBlendFactor.BlendAlpha, + BlendFactor.OneMinusConstantAlpha => MTLBlendFactor.OneMinusBlendAlpha, + _ => LogInvalidAndReturn(factor, nameof(BlendFactor), MTLBlendFactor.Zero) + }; + } + + public static MTLBlendOperation Convert(this BlendOp op) + { + return op switch + { + BlendOp.Add or BlendOp.AddGl => MTLBlendOperation.Add, + BlendOp.Subtract or BlendOp.SubtractGl => MTLBlendOperation.Subtract, + BlendOp.ReverseSubtract or BlendOp.ReverseSubtractGl => MTLBlendOperation.ReverseSubtract, + BlendOp.Minimum => MTLBlendOperation.Min, + BlendOp.Maximum => MTLBlendOperation.Max, + _ => LogInvalidAndReturn(op, nameof(BlendOp), MTLBlendOperation.Add) + }; + } + + public static MTLCompareFunction Convert(this CompareOp op) + { + return op switch + { + CompareOp.Never or CompareOp.NeverGl => MTLCompareFunction.Never, + CompareOp.Less or CompareOp.LessGl => MTLCompareFunction.Less, + CompareOp.Equal or CompareOp.EqualGl => MTLCompareFunction.Equal, + CompareOp.LessOrEqual or CompareOp.LessOrEqualGl => MTLCompareFunction.LessEqual, + CompareOp.Greater or CompareOp.GreaterGl => MTLCompareFunction.Greater, + CompareOp.NotEqual or CompareOp.NotEqualGl => MTLCompareFunction.NotEqual, + CompareOp.GreaterOrEqual or CompareOp.GreaterOrEqualGl => MTLCompareFunction.GreaterEqual, + CompareOp.Always or CompareOp.AlwaysGl => MTLCompareFunction.Always, + _ => LogInvalidAndReturn(op, nameof(CompareOp), MTLCompareFunction.Never) + }; + } + + public static MTLCullMode Convert(this Face face) + { + return face switch + { + Face.Back => MTLCullMode.Back, + Face.Front => MTLCullMode.Front, + Face.FrontAndBack => MTLCullMode.None, + _ => LogInvalidAndReturn(face, nameof(Face), MTLCullMode.Back) + }; + } + + public static MTLWinding Convert(this FrontFace frontFace) + { + // The viewport is flipped vertically, therefore we need to switch the winding order as well + return frontFace switch + { + FrontFace.Clockwise => MTLWinding.CounterClockwise, + FrontFace.CounterClockwise => MTLWinding.Clockwise, + _ => LogInvalidAndReturn(frontFace, nameof(FrontFace), MTLWinding.Clockwise) + }; + } + + public static MTLIndexType Convert(this IndexType type) + { + return type switch + { + IndexType.UShort => MTLIndexType.UInt16, + IndexType.UInt => MTLIndexType.UInt32, + _ => LogInvalidAndReturn(type, nameof(IndexType), MTLIndexType.UInt16) + }; + } + + public static MTLLogicOperation Convert(this LogicalOp op) + { + return op switch + { + LogicalOp.Clear => MTLLogicOperation.Clear, + LogicalOp.And => MTLLogicOperation.And, + LogicalOp.AndReverse => MTLLogicOperation.AndReverse, + LogicalOp.Copy => MTLLogicOperation.Copy, + LogicalOp.AndInverted => MTLLogicOperation.AndInverted, + LogicalOp.Noop => MTLLogicOperation.Noop, + LogicalOp.Xor => MTLLogicOperation.Xor, + LogicalOp.Or => MTLLogicOperation.Or, + LogicalOp.Nor => MTLLogicOperation.Nor, + LogicalOp.Equiv => MTLLogicOperation.Equivalence, + LogicalOp.Invert => MTLLogicOperation.Invert, + LogicalOp.OrReverse => MTLLogicOperation.OrReverse, + LogicalOp.CopyInverted => MTLLogicOperation.CopyInverted, + LogicalOp.OrInverted => MTLLogicOperation.OrInverted, + LogicalOp.Nand => MTLLogicOperation.Nand, + LogicalOp.Set => MTLLogicOperation.Set, + _ => LogInvalidAndReturn(op, nameof(LogicalOp), MTLLogicOperation.And) + }; + } + + public static MTLSamplerMinMagFilter Convert(this MagFilter filter) + { + return filter switch + { + MagFilter.Nearest => MTLSamplerMinMagFilter.Nearest, + MagFilter.Linear => MTLSamplerMinMagFilter.Linear, + _ => LogInvalidAndReturn(filter, nameof(MagFilter), MTLSamplerMinMagFilter.Nearest) + }; + } + + public static (MTLSamplerMinMagFilter, MTLSamplerMipFilter) Convert(this MinFilter filter) + { + return filter switch + { + MinFilter.Nearest => (MTLSamplerMinMagFilter.Nearest, MTLSamplerMipFilter.Nearest), + MinFilter.Linear => (MTLSamplerMinMagFilter.Linear, MTLSamplerMipFilter.Linear), + MinFilter.NearestMipmapNearest => (MTLSamplerMinMagFilter.Nearest, MTLSamplerMipFilter.Nearest), + MinFilter.LinearMipmapNearest => (MTLSamplerMinMagFilter.Linear, MTLSamplerMipFilter.Nearest), + MinFilter.NearestMipmapLinear => (MTLSamplerMinMagFilter.Nearest, MTLSamplerMipFilter.Linear), + MinFilter.LinearMipmapLinear => (MTLSamplerMinMagFilter.Linear, MTLSamplerMipFilter.Linear), + _ => LogInvalidAndReturn(filter, nameof(MinFilter), (MTLSamplerMinMagFilter.Nearest, MTLSamplerMipFilter.Nearest)) + + }; + } + + public static MTLPrimitiveType Convert(this PrimitiveTopology topology) + { + return topology switch + { + PrimitiveTopology.Points => MTLPrimitiveType.Point, + PrimitiveTopology.Lines => MTLPrimitiveType.Line, + PrimitiveTopology.LineStrip => MTLPrimitiveType.LineStrip, + PrimitiveTopology.Triangles => MTLPrimitiveType.Triangle, + PrimitiveTopology.TriangleStrip => MTLPrimitiveType.TriangleStrip, + _ => LogInvalidAndReturn(topology, nameof(PrimitiveTopology), MTLPrimitiveType.Triangle) + }; + } + + public static MTLStencilOperation Convert(this StencilOp op) + { + return op switch + { + StencilOp.Keep or StencilOp.KeepGl => MTLStencilOperation.Keep, + StencilOp.Zero or StencilOp.ZeroGl => MTLStencilOperation.Zero, + StencilOp.Replace or StencilOp.ReplaceGl => MTLStencilOperation.Replace, + StencilOp.IncrementAndClamp or StencilOp.IncrementAndClampGl => MTLStencilOperation.IncrementClamp, + StencilOp.DecrementAndClamp or StencilOp.DecrementAndClampGl => MTLStencilOperation.DecrementClamp, + StencilOp.Invert or StencilOp.InvertGl => MTLStencilOperation.Invert, + StencilOp.IncrementAndWrap or StencilOp.IncrementAndWrapGl => MTLStencilOperation.IncrementWrap, + StencilOp.DecrementAndWrap or StencilOp.DecrementAndWrapGl => MTLStencilOperation.DecrementWrap, + _ => LogInvalidAndReturn(op, nameof(StencilOp), MTLStencilOperation.Keep) + }; + } + + public static MTLTextureType Convert(this Target target) + { + return target switch + { + Target.TextureBuffer => MTLTextureType.TextureBuffer, + Target.Texture1D => MTLTextureType.Type1D, + Target.Texture1DArray => MTLTextureType.Type1DArray, + Target.Texture2D => MTLTextureType.Type2D, + Target.Texture2DArray => MTLTextureType.Type2DArray, + Target.Texture2DMultisample => MTLTextureType.Type2DMultisample, + Target.Texture2DMultisampleArray => MTLTextureType.Type2DMultisampleArray, + Target.Texture3D => MTLTextureType.Type3D, + Target.Cubemap => MTLTextureType.Cube, + Target.CubemapArray => MTLTextureType.CubeArray, + _ => LogInvalidAndReturn(target, nameof(Target), MTLTextureType.Type2D) + }; + } + + public static MTLTextureSwizzle Convert(this SwizzleComponent swizzleComponent) + { + return swizzleComponent switch + { + SwizzleComponent.Zero => MTLTextureSwizzle.Zero, + SwizzleComponent.One => MTLTextureSwizzle.One, + SwizzleComponent.Red => MTLTextureSwizzle.Red, + SwizzleComponent.Green => MTLTextureSwizzle.Green, + SwizzleComponent.Blue => MTLTextureSwizzle.Blue, + SwizzleComponent.Alpha => MTLTextureSwizzle.Alpha, + _ => LogInvalidAndReturn(swizzleComponent, nameof(SwizzleComponent), MTLTextureSwizzle.Zero) + }; + } + + public static MTLVertexFormat Convert(this Format format) + { + return format switch + { + Format.R16Float => MTLVertexFormat.Half, + Format.R16G16Float => MTLVertexFormat.Half2, + Format.R16G16B16Float => MTLVertexFormat.Half3, + Format.R16G16B16A16Float => MTLVertexFormat.Half4, + Format.R32Float => MTLVertexFormat.Float, + Format.R32G32Float => MTLVertexFormat.Float2, + Format.R32G32B32Float => MTLVertexFormat.Float3, + Format.R11G11B10Float => MTLVertexFormat.FloatRG11B10, + Format.R32G32B32A32Float => MTLVertexFormat.Float4, + Format.R8Uint => MTLVertexFormat.UChar, + Format.R8G8Uint => MTLVertexFormat.UChar2, + Format.R8G8B8Uint => MTLVertexFormat.UChar3, + Format.R8G8B8A8Uint => MTLVertexFormat.UChar4, + Format.R16Uint => MTLVertexFormat.UShort, + Format.R16G16Uint => MTLVertexFormat.UShort2, + Format.R16G16B16Uint => MTLVertexFormat.UShort3, + Format.R16G16B16A16Uint => MTLVertexFormat.UShort4, + Format.R32Uint => MTLVertexFormat.UInt, + Format.R32G32Uint => MTLVertexFormat.UInt2, + Format.R32G32B32Uint => MTLVertexFormat.UInt3, + Format.R32G32B32A32Uint => MTLVertexFormat.UInt4, + Format.R8Sint => MTLVertexFormat.Char, + Format.R8G8Sint => MTLVertexFormat.Char2, + Format.R8G8B8Sint => MTLVertexFormat.Char3, + Format.R8G8B8A8Sint => MTLVertexFormat.Char4, + Format.R16Sint => MTLVertexFormat.Short, + Format.R16G16Sint => MTLVertexFormat.Short2, + Format.R16G16B16Sint => MTLVertexFormat.Short3, + Format.R16G16B16A16Sint => MTLVertexFormat.Short4, + Format.R32Sint => MTLVertexFormat.Int, + Format.R32G32Sint => MTLVertexFormat.Int2, + Format.R32G32B32Sint => MTLVertexFormat.Int3, + Format.R32G32B32A32Sint => MTLVertexFormat.Int4, + Format.R8Unorm => MTLVertexFormat.UCharNormalized, + Format.R8G8Unorm => MTLVertexFormat.UChar2Normalized, + Format.R8G8B8Unorm => MTLVertexFormat.UChar3Normalized, + Format.R8G8B8A8Unorm => MTLVertexFormat.UChar4Normalized, + Format.R16Unorm => MTLVertexFormat.UShortNormalized, + Format.R16G16Unorm => MTLVertexFormat.UShort2Normalized, + Format.R16G16B16Unorm => MTLVertexFormat.UShort3Normalized, + Format.R16G16B16A16Unorm => MTLVertexFormat.UShort4Normalized, + Format.R10G10B10A2Unorm => MTLVertexFormat.UInt1010102Normalized, + Format.R8Snorm => MTLVertexFormat.CharNormalized, + Format.R8G8Snorm => MTLVertexFormat.Char2Normalized, + Format.R8G8B8Snorm => MTLVertexFormat.Char3Normalized, + Format.R8G8B8A8Snorm => MTLVertexFormat.Char4Normalized, + Format.R16Snorm => MTLVertexFormat.ShortNormalized, + Format.R16G16Snorm => MTLVertexFormat.Short2Normalized, + Format.R16G16B16Snorm => MTLVertexFormat.Short3Normalized, + Format.R16G16B16A16Snorm => MTLVertexFormat.Short4Normalized, + Format.R10G10B10A2Snorm => MTLVertexFormat.Int1010102Normalized, + + _ => LogInvalidAndReturn(format, nameof(Format), MTLVertexFormat.Float4) + }; + } + + private static T2 LogInvalidAndReturn(T1 value, string name, T2 defaultValue = default) + { + Logger.Debug?.Print(LogClass.Gpu, $"Invalid {name} enum value: {value}."); + + return defaultValue; + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/FenceHolder.cs b/src/Ryujinx.Graphics.Metal/FenceHolder.cs new file mode 100644 index 000000000..a8dd28c0d --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/FenceHolder.cs @@ -0,0 +1,77 @@ +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; +using System.Threading; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + class FenceHolder : IDisposable + { + private MTLCommandBuffer _fence; + private int _referenceCount; + private bool _disposed; + + public FenceHolder(MTLCommandBuffer fence) + { + _fence = fence; + _referenceCount = 1; + } + + public MTLCommandBuffer GetUnsafe() + { + return _fence; + } + + public bool TryGet(out MTLCommandBuffer fence) + { + int lastValue; + do + { + lastValue = _referenceCount; + + if (lastValue == 0) + { + fence = default; + return false; + } + } while (Interlocked.CompareExchange(ref _referenceCount, lastValue + 1, lastValue) != lastValue); + + fence = _fence; + return true; + } + + public MTLCommandBuffer Get() + { + Interlocked.Increment(ref _referenceCount); + return _fence; + } + + public void Put() + { + if (Interlocked.Decrement(ref _referenceCount) == 0) + { + _fence = default; + } + } + + public void Wait() + { + _fence.WaitUntilCompleted(); + } + + public bool IsSignaled() + { + return _fence.Status == MTLCommandBufferStatus.Completed; + } + + public void Dispose() + { + if (!_disposed) + { + Put(); + _disposed = true; + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/FormatConverter.cs b/src/Ryujinx.Graphics.Metal/FormatConverter.cs new file mode 100644 index 000000000..e099187b8 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/FormatConverter.cs @@ -0,0 +1,49 @@ +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Graphics.Metal +{ + class FormatConverter + { + public static void ConvertD24S8ToD32FS8(Span output, ReadOnlySpan input) + { + const float UnormToFloat = 1f / 0xffffff; + + Span outputUint = MemoryMarshal.Cast(output); + ReadOnlySpan inputUint = MemoryMarshal.Cast(input); + + int i = 0; + + for (; i < inputUint.Length; i++) + { + uint depthStencil = inputUint[i]; + uint depth = depthStencil >> 8; + uint stencil = depthStencil & 0xff; + + int j = i * 2; + + outputUint[j] = (uint)BitConverter.SingleToInt32Bits(depth * UnormToFloat); + outputUint[j + 1] = stencil; + } + } + + public static void ConvertD32FS8ToD24S8(Span output, ReadOnlySpan input) + { + Span outputUint = MemoryMarshal.Cast(output); + ReadOnlySpan inputUint = MemoryMarshal.Cast(input); + + int i = 0; + + for (; i < inputUint.Length; i += 2) + { + float depth = BitConverter.Int32BitsToSingle((int)inputUint[i]); + uint stencil = inputUint[i + 1]; + uint depthStencil = (Math.Clamp((uint)(depth * 0xffffff), 0, 0xffffff) << 8) | (stencil & 0xff); + + int j = i >> 1; + + outputUint[j] = depthStencil; + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/FormatTable.cs b/src/Ryujinx.Graphics.Metal/FormatTable.cs new file mode 100644 index 000000000..c1f8923f9 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/FormatTable.cs @@ -0,0 +1,196 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.GAL; +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + static class FormatTable + { + private static readonly MTLPixelFormat[] _table; + + static FormatTable() + { + _table = new MTLPixelFormat[Enum.GetNames(typeof(Format)).Length]; + + Add(Format.R8Unorm, MTLPixelFormat.R8Unorm); + Add(Format.R8Snorm, MTLPixelFormat.R8Snorm); + Add(Format.R8Uint, MTLPixelFormat.R8Uint); + Add(Format.R8Sint, MTLPixelFormat.R8Sint); + Add(Format.R16Float, MTLPixelFormat.R16Float); + Add(Format.R16Unorm, MTLPixelFormat.R16Unorm); + Add(Format.R16Snorm, MTLPixelFormat.R16Snorm); + Add(Format.R16Uint, MTLPixelFormat.R16Uint); + Add(Format.R16Sint, MTLPixelFormat.R16Sint); + Add(Format.R32Float, MTLPixelFormat.R32Float); + Add(Format.R32Uint, MTLPixelFormat.R32Uint); + Add(Format.R32Sint, MTLPixelFormat.R32Sint); + Add(Format.R8G8Unorm, MTLPixelFormat.RG8Unorm); + Add(Format.R8G8Snorm, MTLPixelFormat.RG8Snorm); + Add(Format.R8G8Uint, MTLPixelFormat.RG8Uint); + Add(Format.R8G8Sint, MTLPixelFormat.RG8Sint); + Add(Format.R16G16Float, MTLPixelFormat.RG16Float); + Add(Format.R16G16Unorm, MTLPixelFormat.RG16Unorm); + Add(Format.R16G16Snorm, MTLPixelFormat.RG16Snorm); + Add(Format.R16G16Uint, MTLPixelFormat.RG16Uint); + Add(Format.R16G16Sint, MTLPixelFormat.RG16Sint); + Add(Format.R32G32Float, MTLPixelFormat.RG32Float); + Add(Format.R32G32Uint, MTLPixelFormat.RG32Uint); + Add(Format.R32G32Sint, MTLPixelFormat.RG32Sint); + // Add(Format.R8G8B8Unorm, MTLPixelFormat.R8G8B8Unorm); + // Add(Format.R8G8B8Snorm, MTLPixelFormat.R8G8B8Snorm); + // Add(Format.R8G8B8Uint, MTLPixelFormat.R8G8B8Uint); + // Add(Format.R8G8B8Sint, MTLPixelFormat.R8G8B8Sint); + // Add(Format.R16G16B16Float, MTLPixelFormat.R16G16B16Float); + // Add(Format.R16G16B16Unorm, MTLPixelFormat.R16G16B16Unorm); + // Add(Format.R16G16B16Snorm, MTLPixelFormat.R16G16B16SNorm); + // Add(Format.R16G16B16Uint, MTLPixelFormat.R16G16B16Uint); + // Add(Format.R16G16B16Sint, MTLPixelFormat.R16G16B16Sint); + // Add(Format.R32G32B32Float, MTLPixelFormat.R32G32B32Sfloat); + // Add(Format.R32G32B32Uint, MTLPixelFormat.R32G32B32Uint); + // Add(Format.R32G32B32Sint, MTLPixelFormat.R32G32B32Sint); + Add(Format.R8G8B8A8Unorm, MTLPixelFormat.RGBA8Unorm); + Add(Format.R8G8B8A8Snorm, MTLPixelFormat.RGBA8Snorm); + Add(Format.R8G8B8A8Uint, MTLPixelFormat.RGBA8Uint); + Add(Format.R8G8B8A8Sint, MTLPixelFormat.RGBA8Sint); + Add(Format.R16G16B16A16Float, MTLPixelFormat.RGBA16Float); + Add(Format.R16G16B16A16Unorm, MTLPixelFormat.RGBA16Unorm); + Add(Format.R16G16B16A16Snorm, MTLPixelFormat.RGBA16Snorm); + Add(Format.R16G16B16A16Uint, MTLPixelFormat.RGBA16Uint); + Add(Format.R16G16B16A16Sint, MTLPixelFormat.RGBA16Sint); + Add(Format.R32G32B32A32Float, MTLPixelFormat.RGBA32Float); + Add(Format.R32G32B32A32Uint, MTLPixelFormat.RGBA32Uint); + Add(Format.R32G32B32A32Sint, MTLPixelFormat.RGBA32Sint); + Add(Format.S8Uint, MTLPixelFormat.Stencil8); + Add(Format.D16Unorm, MTLPixelFormat.Depth16Unorm); + Add(Format.S8UintD24Unorm, MTLPixelFormat.Depth24UnormStencil8); + Add(Format.X8UintD24Unorm, MTLPixelFormat.Depth24UnormStencil8); + Add(Format.D32Float, MTLPixelFormat.Depth32Float); + Add(Format.D24UnormS8Uint, MTLPixelFormat.Depth24UnormStencil8); + Add(Format.D32FloatS8Uint, MTLPixelFormat.Depth32FloatStencil8); + Add(Format.R8G8B8A8Srgb, MTLPixelFormat.RGBA8UnormsRGB); + // Add(Format.R4G4Unorm, MTLPixelFormat.R4G4Unorm); + Add(Format.R4G4B4A4Unorm, MTLPixelFormat.RGBA8Unorm); + // Add(Format.R5G5B5X1Unorm, MTLPixelFormat.R5G5B5X1Unorm); + Add(Format.R5G5B5A1Unorm, MTLPixelFormat.BGR5A1Unorm); + Add(Format.R5G6B5Unorm, MTLPixelFormat.B5G6R5Unorm); + Add(Format.R10G10B10A2Unorm, MTLPixelFormat.RGB10A2Unorm); + Add(Format.R10G10B10A2Uint, MTLPixelFormat.RGB10A2Uint); + Add(Format.R11G11B10Float, MTLPixelFormat.RG11B10Float); + Add(Format.R9G9B9E5Float, MTLPixelFormat.RGB9E5Float); + Add(Format.Bc1RgbaUnorm, MTLPixelFormat.BC1RGBA); + Add(Format.Bc2Unorm, MTLPixelFormat.BC2RGBA); + Add(Format.Bc3Unorm, MTLPixelFormat.BC3RGBA); + Add(Format.Bc1RgbaSrgb, MTLPixelFormat.BC1RGBAsRGB); + Add(Format.Bc2Srgb, MTLPixelFormat.BC2RGBAsRGB); + Add(Format.Bc3Srgb, MTLPixelFormat.BC3RGBAsRGB); + Add(Format.Bc4Unorm, MTLPixelFormat.BC4RUnorm); + Add(Format.Bc4Snorm, MTLPixelFormat.BC4RSnorm); + Add(Format.Bc5Unorm, MTLPixelFormat.BC5RGUnorm); + Add(Format.Bc5Snorm, MTLPixelFormat.BC5RGSnorm); + Add(Format.Bc7Unorm, MTLPixelFormat.BC7RGBAUnorm); + Add(Format.Bc7Srgb, MTLPixelFormat.BC7RGBAUnormsRGB); + Add(Format.Bc6HSfloat, MTLPixelFormat.BC6HRGBFloat); + Add(Format.Bc6HUfloat, MTLPixelFormat.BC6HRGBUfloat); + Add(Format.Etc2RgbUnorm, MTLPixelFormat.ETC2RGB8); + // Add(Format.Etc2RgbaUnorm, MTLPixelFormat.ETC2RGBA8); + Add(Format.Etc2RgbPtaUnorm, MTLPixelFormat.ETC2RGB8A1); + Add(Format.Etc2RgbSrgb, MTLPixelFormat.ETC2RGB8sRGB); + // Add(Format.Etc2RgbaSrgb, MTLPixelFormat.ETC2RGBA8sRGB); + Add(Format.Etc2RgbPtaSrgb, MTLPixelFormat.ETC2RGB8A1sRGB); + // Add(Format.R8Uscaled, MTLPixelFormat.R8Uscaled); + // Add(Format.R8Sscaled, MTLPixelFormat.R8Sscaled); + // Add(Format.R16Uscaled, MTLPixelFormat.R16Uscaled); + // Add(Format.R16Sscaled, MTLPixelFormat.R16Sscaled); + // Add(Format.R32Uscaled, MTLPixelFormat.R32Uscaled); + // Add(Format.R32Sscaled, MTLPixelFormat.R32Sscaled); + // Add(Format.R8G8Uscaled, MTLPixelFormat.R8G8Uscaled); + // Add(Format.R8G8Sscaled, MTLPixelFormat.R8G8Sscaled); + // Add(Format.R16G16Uscaled, MTLPixelFormat.R16G16Uscaled); + // Add(Format.R16G16Sscaled, MTLPixelFormat.R16G16Sscaled); + // Add(Format.R32G32Uscaled, MTLPixelFormat.R32G32Uscaled); + // Add(Format.R32G32Sscaled, MTLPixelFormat.R32G32Sscaled); + // Add(Format.R8G8B8Uscaled, MTLPixelFormat.R8G8B8Uscaled); + // Add(Format.R8G8B8Sscaled, MTLPixelFormat.R8G8B8Sscaled); + // Add(Format.R16G16B16Uscaled, MTLPixelFormat.R16G16B16Uscaled); + // Add(Format.R16G16B16Sscaled, MTLPixelFormat.R16G16B16Sscaled); + // Add(Format.R32G32B32Uscaled, MTLPixelFormat.R32G32B32Uscaled); + // Add(Format.R32G32B32Sscaled, MTLPixelFormat.R32G32B32Sscaled); + // Add(Format.R8G8B8A8Uscaled, MTLPixelFormat.R8G8B8A8Uscaled); + // Add(Format.R8G8B8A8Sscaled, MTLPixelFormat.R8G8B8A8Sscaled); + // Add(Format.R16G16B16A16Uscaled, MTLPixelFormat.R16G16B16A16Uscaled); + // Add(Format.R16G16B16A16Sscaled, MTLPixelFormat.R16G16B16A16Sscaled); + // Add(Format.R32G32B32A32Uscaled, MTLPixelFormat.R32G32B32A32Uscaled); + // Add(Format.R32G32B32A32Sscaled, MTLPixelFormat.R32G32B32A32Sscaled); + // Add(Format.R10G10B10A2Snorm, MTLPixelFormat.A2B10G10R10SNormPack32); + // Add(Format.R10G10B10A2Sint, MTLPixelFormat.A2B10G10R10SintPack32); + // Add(Format.R10G10B10A2Uscaled, MTLPixelFormat.A2B10G10R10UscaledPack32); + // Add(Format.R10G10B10A2Sscaled, MTLPixelFormat.A2B10G10R10SscaledPack32); + Add(Format.Astc4x4Unorm, MTLPixelFormat.ASTC4x4LDR); + Add(Format.Astc5x4Unorm, MTLPixelFormat.ASTC5x4LDR); + Add(Format.Astc5x5Unorm, MTLPixelFormat.ASTC5x5LDR); + Add(Format.Astc6x5Unorm, MTLPixelFormat.ASTC6x5LDR); + Add(Format.Astc6x6Unorm, MTLPixelFormat.ASTC6x6LDR); + Add(Format.Astc8x5Unorm, MTLPixelFormat.ASTC8x5LDR); + Add(Format.Astc8x6Unorm, MTLPixelFormat.ASTC8x6LDR); + Add(Format.Astc8x8Unorm, MTLPixelFormat.ASTC8x8LDR); + Add(Format.Astc10x5Unorm, MTLPixelFormat.ASTC10x5LDR); + Add(Format.Astc10x6Unorm, MTLPixelFormat.ASTC10x6LDR); + Add(Format.Astc10x8Unorm, MTLPixelFormat.ASTC10x8LDR); + Add(Format.Astc10x10Unorm, MTLPixelFormat.ASTC10x10LDR); + Add(Format.Astc12x10Unorm, MTLPixelFormat.ASTC12x10LDR); + Add(Format.Astc12x12Unorm, MTLPixelFormat.ASTC12x12LDR); + Add(Format.Astc4x4Srgb, MTLPixelFormat.ASTC4x4sRGB); + Add(Format.Astc5x4Srgb, MTLPixelFormat.ASTC5x4sRGB); + Add(Format.Astc5x5Srgb, MTLPixelFormat.ASTC5x5sRGB); + Add(Format.Astc6x5Srgb, MTLPixelFormat.ASTC6x5sRGB); + Add(Format.Astc6x6Srgb, MTLPixelFormat.ASTC6x6sRGB); + Add(Format.Astc8x5Srgb, MTLPixelFormat.ASTC8x5sRGB); + Add(Format.Astc8x6Srgb, MTLPixelFormat.ASTC8x6sRGB); + Add(Format.Astc8x8Srgb, MTLPixelFormat.ASTC8x8sRGB); + Add(Format.Astc10x5Srgb, MTLPixelFormat.ASTC10x5sRGB); + Add(Format.Astc10x6Srgb, MTLPixelFormat.ASTC10x6sRGB); + Add(Format.Astc10x8Srgb, MTLPixelFormat.ASTC10x8sRGB); + Add(Format.Astc10x10Srgb, MTLPixelFormat.ASTC10x10sRGB); + Add(Format.Astc12x10Srgb, MTLPixelFormat.ASTC12x10sRGB); + Add(Format.Astc12x12Srgb, MTLPixelFormat.ASTC12x12sRGB); + Add(Format.B5G6R5Unorm, MTLPixelFormat.B5G6R5Unorm); + Add(Format.B5G5R5A1Unorm, MTLPixelFormat.BGR5A1Unorm); + Add(Format.A1B5G5R5Unorm, MTLPixelFormat.A1BGR5Unorm); + Add(Format.B8G8R8A8Unorm, MTLPixelFormat.BGRA8Unorm); + Add(Format.B8G8R8A8Srgb, MTLPixelFormat.BGRA8UnormsRGB); + } + + private static void Add(Format format, MTLPixelFormat mtlFormat) + { + _table[(int)format] = mtlFormat; + } + + public static MTLPixelFormat GetFormat(Format format) + { + var mtlFormat = _table[(int)format]; + + if (IsD24S8(format)) + { + if (!MTLDevice.CreateSystemDefaultDevice().Depth24Stencil8PixelFormatSupported) + { + mtlFormat = MTLPixelFormat.Depth32FloatStencil8; + } + } + + if (mtlFormat == MTLPixelFormat.Invalid) + { + Logger.Error?.PrintMsg(LogClass.Gpu, $"Format {format} is not supported by the host."); + } + + return mtlFormat; + } + + public static bool IsD24S8(Format format) + { + return format == Format.D24UnormS8Uint || format == Format.S8UintD24Unorm || format == Format.X8UintD24Unorm; + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/HardwareInfo.cs b/src/Ryujinx.Graphics.Metal/HardwareInfo.cs new file mode 100644 index 000000000..4b3b710f8 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/HardwareInfo.cs @@ -0,0 +1,82 @@ +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Graphics.Metal +{ + static partial class HardwareInfoTools + { + + private readonly static IntPtr _kCFAllocatorDefault = IntPtr.Zero; + private readonly static UInt32 _kCFStringEncodingASCII = 0x0600; + private const string IOKit = "/System/Library/Frameworks/IOKit.framework/IOKit"; + private const string CoreFoundation = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation"; + + [LibraryImport(IOKit, StringMarshalling = StringMarshalling.Utf8)] + private static partial IntPtr IOServiceMatching(string name); + + [LibraryImport(IOKit)] + private static partial IntPtr IOServiceGetMatchingService(IntPtr mainPort, IntPtr matching); + + [LibraryImport(IOKit)] + private static partial IntPtr IORegistryEntryCreateCFProperty(IntPtr entry, IntPtr key, IntPtr allocator, UInt32 options); + + [LibraryImport(CoreFoundation, StringMarshalling = StringMarshalling.Utf8)] + private static partial IntPtr CFStringCreateWithCString(IntPtr allocator, string cString, UInt32 encoding); + + [LibraryImport(CoreFoundation)] + [return: MarshalAs(UnmanagedType.U1)] + public static partial bool CFStringGetCString(IntPtr theString, IntPtr buffer, long bufferSizes, UInt32 encoding); + + [LibraryImport(CoreFoundation)] + public static partial IntPtr CFDataGetBytePtr(IntPtr theData); + + static string GetNameFromId(uint id) + { + return id switch + { + 0x1002 => "AMD", + 0x106B => "Apple", + 0x10DE => "NVIDIA", + 0x13B5 => "ARM", + 0x8086 => "Intel", + _ => $"0x{id:X}" + }; + } + + public static string GetVendor() + { + var serviceDict = IOServiceMatching("IOGPU"); + var service = IOServiceGetMatchingService(IntPtr.Zero, serviceDict); + var cfString = CFStringCreateWithCString(_kCFAllocatorDefault, "vendor-id", _kCFStringEncodingASCII); + var cfProperty = IORegistryEntryCreateCFProperty(service, cfString, _kCFAllocatorDefault, 0); + + byte[] buffer = new byte[4]; + var bufferPtr = CFDataGetBytePtr(cfProperty); + Marshal.Copy(bufferPtr, buffer, 0, buffer.Length); + + var vendorId = BitConverter.ToUInt32(buffer); + + return GetNameFromId(vendorId); + } + + public static string GetModel() + { + var serviceDict = IOServiceMatching("IOGPU"); + var service = IOServiceGetMatchingService(IntPtr.Zero, serviceDict); + var cfString = CFStringCreateWithCString(_kCFAllocatorDefault, "model", _kCFStringEncodingASCII); + var cfProperty = IORegistryEntryCreateCFProperty(service, cfString, _kCFAllocatorDefault, 0); + + char[] buffer = new char[64]; + IntPtr bufferPtr = Marshal.AllocHGlobal(buffer.Length); + + if (CFStringGetCString(cfProperty, bufferPtr, buffer.Length, _kCFStringEncodingASCII)) + { + var model = Marshal.PtrToStringUTF8(bufferPtr); + Marshal.FreeHGlobal(bufferPtr); + return model; + } + + return ""; + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/HashTableSlim.cs b/src/Ryujinx.Graphics.Metal/HashTableSlim.cs new file mode 100644 index 000000000..a27a53d47 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/HashTableSlim.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Ryujinx.Graphics.Metal +{ + interface IRefEquatable + { + bool Equals(ref T other); + } + + class HashTableSlim where TKey : IRefEquatable + { + private const int TotalBuckets = 16; // Must be power of 2 + private const int TotalBucketsMask = TotalBuckets - 1; + + private struct Entry + { + public int Hash; + public TKey Key; + public TValue Value; + } + + private struct Bucket + { + public int Length; + public Entry[] Entries; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public readonly Span AsSpan() + { + return Entries == null ? Span.Empty : Entries.AsSpan(0, Length); + } + } + + private readonly Bucket[] _hashTable = new Bucket[TotalBuckets]; + + public IEnumerable Keys + { + get + { + foreach (Bucket bucket in _hashTable) + { + for (int i = 0; i < bucket.Length; i++) + { + yield return bucket.Entries[i].Key; + } + } + } + } + + public IEnumerable Values + { + get + { + foreach (Bucket bucket in _hashTable) + { + for (int i = 0; i < bucket.Length; i++) + { + yield return bucket.Entries[i].Value; + } + } + } + } + + public void Add(ref TKey key, TValue value) + { + var entry = new Entry + { + Hash = key.GetHashCode(), + Key = key, + Value = value, + }; + + int hashCode = key.GetHashCode(); + int bucketIndex = hashCode & TotalBucketsMask; + + ref var bucket = ref _hashTable[bucketIndex]; + if (bucket.Entries != null) + { + int index = bucket.Length; + + if (index >= bucket.Entries.Length) + { + Array.Resize(ref bucket.Entries, index + 1); + } + + bucket.Entries[index] = entry; + } + else + { + bucket.Entries = new[] + { + entry, + }; + } + + bucket.Length++; + } + + public bool Remove(ref TKey key) + { + int hashCode = key.GetHashCode(); + + ref var bucket = ref _hashTable[hashCode & TotalBucketsMask]; + var entries = bucket.AsSpan(); + for (int i = 0; i < entries.Length; i++) + { + ref var entry = ref entries[i]; + + if (entry.Hash == hashCode && entry.Key.Equals(ref key)) + { + entries[(i + 1)..].CopyTo(entries[i..]); + bucket.Length--; + + return true; + } + } + + return false; + } + + public bool TryGetValue(ref TKey key, out TValue value) + { + int hashCode = key.GetHashCode(); + + var entries = _hashTable[hashCode & TotalBucketsMask].AsSpan(); + for (int i = 0; i < entries.Length; i++) + { + ref var entry = ref entries[i]; + + if (entry.Hash == hashCode && entry.Key.Equals(ref key)) + { + value = entry.Value; + return true; + } + } + + value = default; + return false; + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/HelperShader.cs b/src/Ryujinx.Graphics.Metal/HelperShader.cs new file mode 100644 index 000000000..53f503207 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/HelperShader.cs @@ -0,0 +1,868 @@ +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Shader; +using Ryujinx.Graphics.Shader.Translation; +using SharpMetal.Metal; +using System; +using System.Collections.Generic; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + class HelperShader : IDisposable + { + private const int ConvertElementsPerWorkgroup = 32 * 100; // Work group size of 32 times 100 elements. + private const string ShadersSourcePath = "/Ryujinx.Graphics.Metal/Shaders"; + private readonly MetalRenderer _renderer; + private readonly Pipeline _pipeline; + private MTLDevice _device; + + private readonly ISampler _samplerLinear; + private readonly ISampler _samplerNearest; + private readonly IProgram _programColorBlitF; + private readonly IProgram _programColorBlitI; + private readonly IProgram _programColorBlitU; + private readonly IProgram _programColorBlitMsF; + private readonly IProgram _programColorBlitMsI; + private readonly IProgram _programColorBlitMsU; + private readonly List _programsColorClearF = new(); + private readonly List _programsColorClearI = new(); + private readonly List _programsColorClearU = new(); + private readonly IProgram _programDepthStencilClear; + private readonly IProgram _programStrideChange; + private readonly IProgram _programConvertD32S8ToD24S8; + private readonly IProgram _programConvertIndexBuffer; + private readonly IProgram _programDepthBlit; + private readonly IProgram _programDepthBlitMs; + private readonly IProgram _programStencilBlit; + private readonly IProgram _programStencilBlitMs; + + private readonly EncoderState _helperShaderState = new(); + + public HelperShader(MTLDevice device, MetalRenderer renderer, Pipeline pipeline) + { + _device = device; + _renderer = renderer; + _pipeline = pipeline; + + _samplerNearest = new SamplerHolder(renderer, _device, SamplerCreateInfo.Create(MinFilter.Nearest, MagFilter.Nearest)); + _samplerLinear = new SamplerHolder(renderer, _device, SamplerCreateInfo.Create(MinFilter.Linear, MagFilter.Linear)); + + var blitResourceLayout = new ResourceLayoutBuilder() + .Add(ResourceStages.Vertex, ResourceType.UniformBuffer, 0) + .Add(ResourceStages.Fragment, ResourceType.TextureAndSampler, 0).Build(); + + var blitSource = ReadMsl("Blit.metal"); + + var blitSourceF = blitSource.Replace("FORMAT", "float", StringComparison.Ordinal); + _programColorBlitF = new Program(renderer, device, [ + new ShaderSource(blitSourceF, ShaderStage.Fragment, TargetLanguage.Msl), + new ShaderSource(blitSourceF, ShaderStage.Vertex, TargetLanguage.Msl) + ], blitResourceLayout); + + var blitSourceI = blitSource.Replace("FORMAT", "int"); + _programColorBlitI = new Program(renderer, device, [ + new ShaderSource(blitSourceI, ShaderStage.Fragment, TargetLanguage.Msl), + new ShaderSource(blitSourceI, ShaderStage.Vertex, TargetLanguage.Msl) + ], blitResourceLayout); + + var blitSourceU = blitSource.Replace("FORMAT", "uint"); + _programColorBlitU = new Program(renderer, device, [ + new ShaderSource(blitSourceU, ShaderStage.Fragment, TargetLanguage.Msl), + new ShaderSource(blitSourceU, ShaderStage.Vertex, TargetLanguage.Msl) + ], blitResourceLayout); + + var blitMsSource = ReadMsl("BlitMs.metal"); + + var blitMsSourceF = blitMsSource.Replace("FORMAT", "float"); + _programColorBlitMsF = new Program(renderer, device, [ + new ShaderSource(blitMsSourceF, ShaderStage.Fragment, TargetLanguage.Msl), + new ShaderSource(blitMsSourceF, ShaderStage.Vertex, TargetLanguage.Msl) + ], blitResourceLayout); + + var blitMsSourceI = blitMsSource.Replace("FORMAT", "int"); + _programColorBlitMsI = new Program(renderer, device, [ + new ShaderSource(blitMsSourceI, ShaderStage.Fragment, TargetLanguage.Msl), + new ShaderSource(blitMsSourceI, ShaderStage.Vertex, TargetLanguage.Msl) + ], blitResourceLayout); + + var blitMsSourceU = blitMsSource.Replace("FORMAT", "uint"); + _programColorBlitMsU = new Program(renderer, device, [ + new ShaderSource(blitMsSourceU, ShaderStage.Fragment, TargetLanguage.Msl), + new ShaderSource(blitMsSourceU, ShaderStage.Vertex, TargetLanguage.Msl) + ], blitResourceLayout); + + var colorClearResourceLayout = new ResourceLayoutBuilder() + .Add(ResourceStages.Fragment, ResourceType.UniformBuffer, 0).Build(); + + var colorClearSource = ReadMsl("ColorClear.metal"); + + for (int i = 0; i < Constants.MaxColorAttachments; i++) + { + var crntSource = colorClearSource.Replace("COLOR_ATTACHMENT_INDEX", i.ToString()).Replace("FORMAT", "float"); + _programsColorClearF.Add(new Program(renderer, device, [ + new ShaderSource(crntSource, ShaderStage.Fragment, TargetLanguage.Msl), + new ShaderSource(crntSource, ShaderStage.Vertex, TargetLanguage.Msl) + ], colorClearResourceLayout)); + } + + for (int i = 0; i < Constants.MaxColorAttachments; i++) + { + var crntSource = colorClearSource.Replace("COLOR_ATTACHMENT_INDEX", i.ToString()).Replace("FORMAT", "int"); + _programsColorClearI.Add(new Program(renderer, device, [ + new ShaderSource(crntSource, ShaderStage.Fragment, TargetLanguage.Msl), + new ShaderSource(crntSource, ShaderStage.Vertex, TargetLanguage.Msl) + ], colorClearResourceLayout)); + } + + for (int i = 0; i < Constants.MaxColorAttachments; i++) + { + var crntSource = colorClearSource.Replace("COLOR_ATTACHMENT_INDEX", i.ToString()).Replace("FORMAT", "uint"); + _programsColorClearU.Add(new Program(renderer, device, [ + new ShaderSource(crntSource, ShaderStage.Fragment, TargetLanguage.Msl), + new ShaderSource(crntSource, ShaderStage.Vertex, TargetLanguage.Msl) + ], colorClearResourceLayout)); + } + + var depthStencilClearSource = ReadMsl("DepthStencilClear.metal"); + _programDepthStencilClear = new Program(renderer, device, [ + new ShaderSource(depthStencilClearSource, ShaderStage.Fragment, TargetLanguage.Msl), + new ShaderSource(depthStencilClearSource, ShaderStage.Vertex, TargetLanguage.Msl) + ], colorClearResourceLayout); + + var strideChangeResourceLayout = new ResourceLayoutBuilder() + .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 0) + .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 1) + .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 2, true).Build(); + + var strideChangeSource = ReadMsl("ChangeBufferStride.metal"); + _programStrideChange = new Program(renderer, device, [ + new ShaderSource(strideChangeSource, ShaderStage.Compute, TargetLanguage.Msl) + ], strideChangeResourceLayout, new ComputeSize(64, 1, 1)); + + var convertD32S8ToD24S8ResourceLayout = new ResourceLayoutBuilder() + .Add(ResourceStages.Compute, ResourceType.UniformBuffer, 0) + .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 1) + .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 2, true).Build(); + + var convertD32S8ToD24S8Source = ReadMsl("ConvertD32S8ToD24S8.metal"); + _programConvertD32S8ToD24S8 = new Program(renderer, device, [ + new ShaderSource(convertD32S8ToD24S8Source, ShaderStage.Compute, TargetLanguage.Msl) + ], convertD32S8ToD24S8ResourceLayout, new ComputeSize(64, 1, 1)); + + var convertIndexBufferLayout = new ResourceLayoutBuilder() + .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 1) + .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 2, true) + .Add(ResourceStages.Compute, ResourceType.StorageBuffer, 3).Build(); + + var convertIndexBufferSource = ReadMsl("ConvertIndexBuffer.metal"); + _programConvertIndexBuffer = new Program(renderer, device, [ + new ShaderSource(convertIndexBufferSource, ShaderStage.Compute, TargetLanguage.Msl) + ], convertIndexBufferLayout, new ComputeSize(16, 1, 1)); + + var depthBlitSource = ReadMsl("DepthBlit.metal"); + _programDepthBlit = new Program(renderer, device, [ + new ShaderSource(depthBlitSource, ShaderStage.Fragment, TargetLanguage.Msl), + new ShaderSource(blitSourceF, ShaderStage.Vertex, TargetLanguage.Msl) + ], blitResourceLayout); + + var depthBlitMsSource = ReadMsl("DepthBlitMs.metal"); + _programDepthBlitMs = new Program(renderer, device, [ + new ShaderSource(depthBlitMsSource, ShaderStage.Fragment, TargetLanguage.Msl), + new ShaderSource(blitSourceF, ShaderStage.Vertex, TargetLanguage.Msl) + ], blitResourceLayout); + + var stencilBlitSource = ReadMsl("StencilBlit.metal"); + _programStencilBlit = new Program(renderer, device, [ + new ShaderSource(stencilBlitSource, ShaderStage.Fragment, TargetLanguage.Msl), + new ShaderSource(blitSourceF, ShaderStage.Vertex, TargetLanguage.Msl) + ], blitResourceLayout); + + var stencilBlitMsSource = ReadMsl("StencilBlitMs.metal"); + _programStencilBlitMs = new Program(renderer, device, [ + new ShaderSource(stencilBlitMsSource, ShaderStage.Fragment, TargetLanguage.Msl), + new ShaderSource(blitSourceF, ShaderStage.Vertex, TargetLanguage.Msl) + ], blitResourceLayout); + } + + private static string ReadMsl(string fileName) + { + var msl = EmbeddedResources.ReadAllText(string.Join('/', ShadersSourcePath, fileName)); + +#pragma warning disable IDE0055 // Disable formatting + msl = msl.Replace("CONSTANT_BUFFERS_INDEX", $"{Constants.ConstantBuffersIndex}") + .Replace("STORAGE_BUFFERS_INDEX", $"{Constants.StorageBuffersIndex}") + .Replace("TEXTURES_INDEX", $"{Constants.TexturesIndex}") + .Replace("IMAGES_INDEX", $"{Constants.ImagesIndex}"); +#pragma warning restore IDE0055 + + return msl; + } + + public unsafe void BlitColor( + CommandBufferScoped cbs, + Texture src, + Texture dst, + Extents2D srcRegion, + Extents2D dstRegion, + bool linearFilter, + bool clear = false) + { + _pipeline.SwapState(_helperShaderState); + + const int RegionBufferSize = 16; + + var sampler = linearFilter ? _samplerLinear : _samplerNearest; + + _pipeline.SetTextureAndSampler(ShaderStage.Fragment, 0, src, sampler); + + Span region = stackalloc float[RegionBufferSize / sizeof(float)]; + + region[0] = srcRegion.X1 / (float)src.Width; + region[1] = srcRegion.X2 / (float)src.Width; + region[2] = srcRegion.Y1 / (float)src.Height; + region[3] = srcRegion.Y2 / (float)src.Height; + + if (dstRegion.X1 > dstRegion.X2) + { + (region[0], region[1]) = (region[1], region[0]); + } + + if (dstRegion.Y1 > dstRegion.Y2) + { + (region[2], region[3]) = (region[3], region[2]); + } + + using var buffer = _renderer.BufferManager.ReserveOrCreate(cbs, RegionBufferSize); + buffer.Holder.SetDataUnchecked(buffer.Offset, region); + _pipeline.SetUniformBuffers([new BufferAssignment(0, buffer.Range)]); + + var rect = new Rectangle( + MathF.Min(dstRegion.X1, dstRegion.X2), + MathF.Min(dstRegion.Y1, dstRegion.Y2), + MathF.Abs(dstRegion.X2 - dstRegion.X1), + MathF.Abs(dstRegion.Y2 - dstRegion.Y1)); + + Span viewports = stackalloc Viewport[16]; + + viewports[0] = new Viewport( + rect, + ViewportSwizzle.PositiveX, + ViewportSwizzle.PositiveY, + ViewportSwizzle.PositiveZ, + ViewportSwizzle.PositiveW, + 0f, + 1f); + + bool dstIsDepthOrStencil = dst.Info.Format.IsDepthOrStencil(); + + if (dstIsDepthOrStencil) + { + // TODO: Depth & stencil blit! + Logger.Warning?.PrintMsg(LogClass.Gpu, "Requested a depth or stencil blit!"); + _pipeline.SwapState(null); + return; + } + + var debugGroupName = "Blit Color "; + + if (src.Info.Target.IsMultisample()) + { + if (dst.Info.Format.IsSint()) + { + debugGroupName += "MS Int"; + _pipeline.SetProgram(_programColorBlitMsI); + } + else if (dst.Info.Format.IsUint()) + { + debugGroupName += "MS UInt"; + _pipeline.SetProgram(_programColorBlitMsU); + } + else + { + debugGroupName += "MS Float"; + _pipeline.SetProgram(_programColorBlitMsF); + } + } + else + { + if (dst.Info.Format.IsSint()) + { + debugGroupName += "Int"; + _pipeline.SetProgram(_programColorBlitI); + } + else if (dst.Info.Format.IsUint()) + { + debugGroupName += "UInt"; + _pipeline.SetProgram(_programColorBlitU); + } + else + { + debugGroupName += "Float"; + _pipeline.SetProgram(_programColorBlitF); + } + } + + int dstWidth = dst.Width; + int dstHeight = dst.Height; + + Span> scissors = stackalloc Rectangle[16]; + + scissors[0] = new Rectangle(0, 0, dstWidth, dstHeight); + + _pipeline.SetRenderTargets([dst], null); + _pipeline.SetScissors(scissors); + + _pipeline.SetClearLoadAction(clear); + + _pipeline.SetViewports(viewports); + _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip); + _pipeline.Draw(4, 1, 0, 0, debugGroupName); + + // Cleanup + if (clear) + { + _pipeline.SetClearLoadAction(false); + } + + // Restore previous state + _pipeline.SwapState(null); + } + + public unsafe void BlitDepthStencil( + CommandBufferScoped cbs, + Texture src, + Texture dst, + Extents2D srcRegion, + Extents2D dstRegion) + { + _pipeline.SwapState(_helperShaderState); + + const int RegionBufferSize = 16; + + Span region = stackalloc float[RegionBufferSize / sizeof(float)]; + + region[0] = srcRegion.X1 / (float)src.Width; + region[1] = srcRegion.X2 / (float)src.Width; + region[2] = srcRegion.Y1 / (float)src.Height; + region[3] = srcRegion.Y2 / (float)src.Height; + + if (dstRegion.X1 > dstRegion.X2) + { + (region[0], region[1]) = (region[1], region[0]); + } + + if (dstRegion.Y1 > dstRegion.Y2) + { + (region[2], region[3]) = (region[3], region[2]); + } + + using var buffer = _renderer.BufferManager.ReserveOrCreate(cbs, RegionBufferSize); + buffer.Holder.SetDataUnchecked(buffer.Offset, region); + _pipeline.SetUniformBuffers([new BufferAssignment(0, buffer.Range)]); + + Span viewports = stackalloc Viewport[16]; + + var rect = new Rectangle( + MathF.Min(dstRegion.X1, dstRegion.X2), + MathF.Min(dstRegion.Y1, dstRegion.Y2), + MathF.Abs(dstRegion.X2 - dstRegion.X1), + MathF.Abs(dstRegion.Y2 - dstRegion.Y1)); + + viewports[0] = new Viewport( + rect, + ViewportSwizzle.PositiveX, + ViewportSwizzle.PositiveY, + ViewportSwizzle.PositiveZ, + ViewportSwizzle.PositiveW, + 0f, + 1f); + + int dstWidth = dst.Width; + int dstHeight = dst.Height; + + Span> scissors = stackalloc Rectangle[16]; + + scissors[0] = new Rectangle(0, 0, dstWidth, dstHeight); + + _pipeline.SetRenderTargets([], dst); + _pipeline.SetScissors(scissors); + _pipeline.SetViewports(viewports); + _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip); + + if (src.Info.Format is + Format.D16Unorm or + Format.D32Float or + Format.X8UintD24Unorm or + Format.D24UnormS8Uint or + Format.D32FloatS8Uint or + Format.S8UintD24Unorm) + { + var depthTexture = CreateDepthOrStencilView(src, DepthStencilMode.Depth); + + BlitDepthStencilDraw(depthTexture, isDepth: true); + + if (depthTexture != src) + { + depthTexture.Release(); + } + } + + if (src.Info.Format is + Format.S8Uint or + Format.D24UnormS8Uint or + Format.D32FloatS8Uint or + Format.S8UintD24Unorm) + { + var stencilTexture = CreateDepthOrStencilView(src, DepthStencilMode.Stencil); + + BlitDepthStencilDraw(stencilTexture, isDepth: false); + + if (stencilTexture != src) + { + stencilTexture.Release(); + } + } + + // Restore previous state + _pipeline.SwapState(null); + } + + private static Texture CreateDepthOrStencilView(Texture depthStencilTexture, DepthStencilMode depthStencilMode) + { + if (depthStencilTexture.Info.DepthStencilMode == depthStencilMode) + { + return depthStencilTexture; + } + + return (Texture)depthStencilTexture.CreateView(new TextureCreateInfo( + depthStencilTexture.Info.Width, + depthStencilTexture.Info.Height, + depthStencilTexture.Info.Depth, + depthStencilTexture.Info.Levels, + depthStencilTexture.Info.Samples, + depthStencilTexture.Info.BlockWidth, + depthStencilTexture.Info.BlockHeight, + depthStencilTexture.Info.BytesPerPixel, + depthStencilTexture.Info.Format, + depthStencilMode, + depthStencilTexture.Info.Target, + SwizzleComponent.Red, + SwizzleComponent.Green, + SwizzleComponent.Blue, + SwizzleComponent.Alpha), 0, 0); + } + + private void BlitDepthStencilDraw(Texture src, bool isDepth) + { + // TODO: Check this https://github.com/Ryujinx/Ryujinx/pull/5003/ + _pipeline.SetTextureAndSampler(ShaderStage.Fragment, 0, src, _samplerNearest); + + string debugGroupName; + + if (isDepth) + { + debugGroupName = "Depth Blit"; + _pipeline.SetProgram(src.Info.Target.IsMultisample() ? _programDepthBlitMs : _programDepthBlit); + _pipeline.SetDepthTest(new DepthTestDescriptor(true, true, CompareOp.Always)); + } + else + { + debugGroupName = "Stencil Blit"; + _pipeline.SetProgram(src.Info.Target.IsMultisample() ? _programStencilBlitMs : _programStencilBlit); + _pipeline.SetStencilTest(CreateStencilTestDescriptor(true)); + } + + _pipeline.Draw(4, 1, 0, 0, debugGroupName); + + if (isDepth) + { + _pipeline.SetDepthTest(new DepthTestDescriptor(false, false, CompareOp.Always)); + } + else + { + _pipeline.SetStencilTest(CreateStencilTestDescriptor(false)); + } + } + + public unsafe void DrawTexture( + ITexture src, + ISampler srcSampler, + Extents2DF srcRegion, + Extents2DF dstRegion) + { + // Save current state + var state = _pipeline.SavePredrawState(); + + _pipeline.SetFaceCulling(false, Face.Front); + _pipeline.SetStencilTest(new StencilTestDescriptor()); + _pipeline.SetDepthTest(new DepthTestDescriptor()); + + const int RegionBufferSize = 16; + + _pipeline.SetTextureAndSampler(ShaderStage.Fragment, 0, src, srcSampler); + + Span region = stackalloc float[RegionBufferSize / sizeof(float)]; + + region[0] = srcRegion.X1 / src.Width; + region[1] = srcRegion.X2 / src.Width; + region[2] = srcRegion.Y1 / src.Height; + region[3] = srcRegion.Y2 / src.Height; + + if (dstRegion.X1 > dstRegion.X2) + { + (region[0], region[1]) = (region[1], region[0]); + } + + if (dstRegion.Y1 > dstRegion.Y2) + { + (region[2], region[3]) = (region[3], region[2]); + } + + var bufferHandle = _renderer.BufferManager.CreateWithHandle(RegionBufferSize); + _renderer.BufferManager.SetData(bufferHandle, 0, region); + _pipeline.SetUniformBuffers([new BufferAssignment(0, new BufferRange(bufferHandle, 0, RegionBufferSize))]); + + Span viewports = stackalloc Viewport[16]; + + var rect = new Rectangle( + MathF.Min(dstRegion.X1, dstRegion.X2), + MathF.Min(dstRegion.Y1, dstRegion.Y2), + MathF.Abs(dstRegion.X2 - dstRegion.X1), + MathF.Abs(dstRegion.Y2 - dstRegion.Y1)); + + viewports[0] = new Viewport( + rect, + ViewportSwizzle.PositiveX, + ViewportSwizzle.PositiveY, + ViewportSwizzle.PositiveZ, + ViewportSwizzle.PositiveW, + 0f, + 1f); + + _pipeline.SetProgram(_programColorBlitF); + _pipeline.SetViewports(viewports); + _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip); + _pipeline.Draw(4, 1, 0, 0, "Draw Texture"); + + _renderer.BufferManager.Delete(bufferHandle); + + // Restore previous state + _pipeline.RestorePredrawState(state); + } + + public void ConvertI8ToI16(CommandBufferScoped cbs, BufferHolder src, BufferHolder dst, int srcOffset, int size) + { + ChangeStride(cbs, src, dst, srcOffset, size, 1, 2); + } + + public unsafe void ChangeStride( + CommandBufferScoped cbs, + BufferHolder src, + BufferHolder dst, + int srcOffset, + int size, + int stride, + int newStride) + { + int elems = size / stride; + + var srcBuffer = src.GetBuffer(); + var dstBuffer = dst.GetBuffer(); + + const int ParamsBufferSize = 4 * sizeof(int); + + // Save current state + _pipeline.SwapState(_helperShaderState); + + Span shaderParams = stackalloc int[ParamsBufferSize / sizeof(int)]; + + shaderParams[0] = stride; + shaderParams[1] = newStride; + shaderParams[2] = size; + shaderParams[3] = srcOffset; + + using var buffer = _renderer.BufferManager.ReserveOrCreate(cbs, ParamsBufferSize); + buffer.Holder.SetDataUnchecked(buffer.Offset, shaderParams); + _pipeline.SetUniformBuffers([new BufferAssignment(0, buffer.Range)]); + + Span> sbRanges = new Auto[2]; + + sbRanges[0] = srcBuffer; + sbRanges[1] = dstBuffer; + _pipeline.SetStorageBuffers(1, sbRanges); + + _pipeline.SetProgram(_programStrideChange); + _pipeline.DispatchCompute(1 + elems / ConvertElementsPerWorkgroup, 1, 1, "Change Stride"); + + // Restore previous state + _pipeline.SwapState(null); + } + + public unsafe void ConvertD32S8ToD24S8(CommandBufferScoped cbs, BufferHolder src, Auto dstBuffer, int pixelCount, int dstOffset) + { + int inSize = pixelCount * 2 * sizeof(int); + + var srcBuffer = src.GetBuffer(); + + const int ParamsBufferSize = sizeof(int) * 2; + + // Save current state + _pipeline.SwapState(_helperShaderState); + + Span shaderParams = stackalloc int[2]; + + shaderParams[0] = pixelCount; + shaderParams[1] = dstOffset; + + using var buffer = _renderer.BufferManager.ReserveOrCreate(cbs, ParamsBufferSize); + buffer.Holder.SetDataUnchecked(buffer.Offset, shaderParams); + _pipeline.SetUniformBuffers([new BufferAssignment(0, buffer.Range)]); + + Span> sbRanges = new Auto[2]; + + sbRanges[0] = srcBuffer; + sbRanges[1] = dstBuffer; + _pipeline.SetStorageBuffers(1, sbRanges); + + _pipeline.SetProgram(_programConvertD32S8ToD24S8); + _pipeline.DispatchCompute(1 + inSize / ConvertElementsPerWorkgroup, 1, 1, "D32S8 to D24S8 Conversion"); + + // Restore previous state + _pipeline.SwapState(null); + } + + public void ConvertIndexBuffer( + CommandBufferScoped cbs, + BufferHolder src, + BufferHolder dst, + IndexBufferPattern pattern, + int indexSize, + int srcOffset, + int indexCount) + { + // TODO: Support conversion with primitive restart enabled. + + int primitiveCount = pattern.GetPrimitiveCount(indexCount); + int outputIndexSize = 4; + + var srcBuffer = src.GetBuffer(); + var dstBuffer = dst.GetBuffer(); + + const int ParamsBufferSize = 16 * sizeof(int); + + // Save current state + _pipeline.SwapState(_helperShaderState); + + Span shaderParams = stackalloc int[ParamsBufferSize / sizeof(int)]; + + shaderParams[8] = pattern.PrimitiveVertices; + shaderParams[9] = pattern.PrimitiveVerticesOut; + shaderParams[10] = indexSize; + shaderParams[11] = outputIndexSize; + shaderParams[12] = pattern.BaseIndex; + shaderParams[13] = pattern.IndexStride; + shaderParams[14] = srcOffset; + shaderParams[15] = primitiveCount; + + pattern.OffsetIndex.CopyTo(shaderParams[..pattern.OffsetIndex.Length]); + + using var patternScoped = _renderer.BufferManager.ReserveOrCreate(cbs, ParamsBufferSize); + patternScoped.Holder.SetDataUnchecked(patternScoped.Offset, shaderParams); + + Span> sbRanges = new Auto[2]; + + sbRanges[0] = srcBuffer; + sbRanges[1] = dstBuffer; + _pipeline.SetStorageBuffers(1, sbRanges); + _pipeline.SetStorageBuffers([new BufferAssignment(3, patternScoped.Range)]); + + _pipeline.SetProgram(_programConvertIndexBuffer); + _pipeline.DispatchCompute(BitUtils.DivRoundUp(primitiveCount, 16), 1, 1, "Convert Index Buffer"); + + // Restore previous state + _pipeline.SwapState(null); + } + + public unsafe void ClearColor( + int index, + ReadOnlySpan clearColor, + uint componentMask, + int dstWidth, + int dstHeight, + Format format) + { + // Keep original scissor + DirtyFlags clearFlags = DirtyFlags.All & (~DirtyFlags.Scissors); + + // Save current state + EncoderState originalState = _pipeline.SwapState(_helperShaderState, clearFlags, false); + + // Inherit some state without fully recreating render pipeline. + RenderTargetCopy save = _helperShaderState.InheritForClear(originalState, false, index); + + const int ClearColorBufferSize = 16; + + // TODO: Flush + + using var buffer = _renderer.BufferManager.ReserveOrCreate(_pipeline.Cbs, ClearColorBufferSize); + buffer.Holder.SetDataUnchecked(buffer.Offset, clearColor); + _pipeline.SetUniformBuffers([new BufferAssignment(0, buffer.Range)]); + + Span viewports = stackalloc Viewport[16]; + + // TODO: Set exact viewport! + viewports[0] = new Viewport( + new Rectangle(0, 0, dstWidth, dstHeight), + ViewportSwizzle.PositiveX, + ViewportSwizzle.PositiveY, + ViewportSwizzle.PositiveZ, + ViewportSwizzle.PositiveW, + 0f, + 1f); + + Span componentMasks = stackalloc uint[index + 1]; + componentMasks[index] = componentMask; + + var debugGroupName = "Clear Color "; + + if (format.IsSint()) + { + debugGroupName += "Int"; + _pipeline.SetProgram(_programsColorClearI[index]); + } + else if (format.IsUint()) + { + debugGroupName += "UInt"; + _pipeline.SetProgram(_programsColorClearU[index]); + } + else + { + debugGroupName += "Float"; + _pipeline.SetProgram(_programsColorClearF[index]); + } + + _pipeline.SetBlendState(index, new BlendDescriptor()); + _pipeline.SetFaceCulling(false, Face.Front); + _pipeline.SetDepthTest(new DepthTestDescriptor(false, false, CompareOp.Always)); + _pipeline.SetRenderTargetColorMasks(componentMasks); + _pipeline.SetViewports(viewports); + _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip); + _pipeline.Draw(4, 1, 0, 0, debugGroupName); + + // Restore previous state + _pipeline.SwapState(null, clearFlags, false); + + _helperShaderState.Restore(save); + } + + public unsafe void ClearDepthStencil( + float depthValue, + bool depthMask, + int stencilValue, + int stencilMask, + int dstWidth, + int dstHeight) + { + // Keep original scissor + DirtyFlags clearFlags = DirtyFlags.All & (~DirtyFlags.Scissors); + var helperScissors = _helperShaderState.Scissors; + + // Save current state + EncoderState originalState = _pipeline.SwapState(_helperShaderState, clearFlags, false); + + // Inherit some state without fully recreating render pipeline. + RenderTargetCopy save = _helperShaderState.InheritForClear(originalState, true); + + const int ClearDepthBufferSize = 16; + + using var buffer = _renderer.BufferManager.ReserveOrCreate(_pipeline.Cbs, ClearDepthBufferSize); + buffer.Holder.SetDataUnchecked(buffer.Offset, new ReadOnlySpan(ref depthValue)); + _pipeline.SetUniformBuffers([new BufferAssignment(0, buffer.Range)]); + + Span viewports = stackalloc Viewport[1]; + + viewports[0] = new Viewport( + new Rectangle(0, 0, dstWidth, dstHeight), + ViewportSwizzle.PositiveX, + ViewportSwizzle.PositiveY, + ViewportSwizzle.PositiveZ, + ViewportSwizzle.PositiveW, + 0f, + 1f); + + _pipeline.SetProgram(_programDepthStencilClear); + _pipeline.SetFaceCulling(false, Face.Front); + _pipeline.SetPrimitiveTopology(PrimitiveTopology.TriangleStrip); + _pipeline.SetViewports(viewports); + _pipeline.SetDepthTest(new DepthTestDescriptor(true, depthMask, CompareOp.Always)); + _pipeline.SetStencilTest(CreateStencilTestDescriptor(stencilMask != 0, stencilValue, 0xFF, stencilMask)); + _pipeline.Draw(4, 1, 0, 0, "Clear Depth Stencil"); + + // Cleanup + _pipeline.SetDepthTest(new DepthTestDescriptor(false, false, CompareOp.Always)); + _pipeline.SetStencilTest(CreateStencilTestDescriptor(false)); + + // Restore previous state + _pipeline.SwapState(null, clearFlags, false); + + _helperShaderState.Restore(save); + } + + private static StencilTestDescriptor CreateStencilTestDescriptor( + bool enabled, + int refValue = 0, + int compareMask = 0xff, + int writeMask = 0xff) + { + return new StencilTestDescriptor( + enabled, + CompareOp.Always, + StencilOp.Replace, + StencilOp.Replace, + StencilOp.Replace, + refValue, + compareMask, + writeMask, + CompareOp.Always, + StencilOp.Replace, + StencilOp.Replace, + StencilOp.Replace, + refValue, + compareMask, + writeMask); + } + + public void Dispose() + { + _programColorBlitF.Dispose(); + _programColorBlitI.Dispose(); + _programColorBlitU.Dispose(); + _programColorBlitMsF.Dispose(); + _programColorBlitMsI.Dispose(); + _programColorBlitMsU.Dispose(); + + foreach (var programColorClear in _programsColorClearF) + { + programColorClear.Dispose(); + } + + foreach (var programColorClear in _programsColorClearU) + { + programColorClear.Dispose(); + } + + foreach (var programColorClear in _programsColorClearI) + { + programColorClear.Dispose(); + } + + _programDepthStencilClear.Dispose(); + _pipeline.Dispose(); + _samplerLinear.Dispose(); + _samplerNearest.Dispose(); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/IdList.cs b/src/Ryujinx.Graphics.Metal/IdList.cs new file mode 100644 index 000000000..2c15a80ef --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/IdList.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; + +namespace Ryujinx.Graphics.Metal +{ + class IdList where T : class + { + private readonly List _list; + private int _freeMin; + + public IdList() + { + _list = new List(); + _freeMin = 0; + } + + public int Add(T value) + { + int id; + int count = _list.Count; + id = _list.IndexOf(null, _freeMin); + + if ((uint)id < (uint)count) + { + _list[id] = value; + } + else + { + id = count; + _freeMin = id + 1; + + _list.Add(value); + } + + return id + 1; + } + + public void Remove(int id) + { + id--; + + int count = _list.Count; + + if ((uint)id >= (uint)count) + { + return; + } + + if (id + 1 == count) + { + // Trim unused items. + int removeIndex = id; + + while (removeIndex > 0 && _list[removeIndex - 1] == null) + { + removeIndex--; + } + + _list.RemoveRange(removeIndex, count - removeIndex); + + if (_freeMin > removeIndex) + { + _freeMin = removeIndex; + } + } + else + { + _list[id] = null; + + if (_freeMin > id) + { + _freeMin = id; + } + } + } + + public bool TryGetValue(int id, out T value) + { + id--; + + try + { + if ((uint)id < (uint)_list.Count) + { + value = _list[id]; + return value != null; + } + + value = null; + return false; + } + catch (ArgumentOutOfRangeException) + { + value = null; + return false; + } + catch (IndexOutOfRangeException) + { + value = null; + return false; + } + } + + public void Clear() + { + _list.Clear(); + _freeMin = 0; + } + + public IEnumerator GetEnumerator() + { + for (int i = 0; i < _list.Count; i++) + { + if (_list[i] != null) + { + yield return _list[i]; + } + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/ImageArray.cs b/src/Ryujinx.Graphics.Metal/ImageArray.cs new file mode 100644 index 000000000..9fa0df09d --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/ImageArray.cs @@ -0,0 +1,74 @@ +using Ryujinx.Graphics.GAL; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + internal class ImageArray : IImageArray + { + private readonly TextureRef[] _textureRefs; + private readonly TextureBuffer[] _bufferTextureRefs; + + private readonly bool _isBuffer; + private readonly Pipeline _pipeline; + + public ImageArray(int size, bool isBuffer, Pipeline pipeline) + { + if (isBuffer) + { + _bufferTextureRefs = new TextureBuffer[size]; + } + else + { + _textureRefs = new TextureRef[size]; + } + + _isBuffer = isBuffer; + _pipeline = pipeline; + } + + public void SetImages(int index, ITexture[] images) + { + for (int i = 0; i < images.Length; i++) + { + ITexture image = images[i]; + + if (image is TextureBuffer textureBuffer) + { + _bufferTextureRefs[index + i] = textureBuffer; + } + else if (image is Texture texture) + { + _textureRefs[index + i].Storage = texture; + } + else if (!_isBuffer) + { + _textureRefs[index + i].Storage = null; + } + else + { + _bufferTextureRefs[index + i] = null; + } + } + + SetDirty(); + } + + public TextureRef[] GetTextureRefs() + { + return _textureRefs; + } + + public TextureBuffer[] GetBufferTextureRefs() + { + return _bufferTextureRefs; + } + + private void SetDirty() + { + _pipeline.DirtyImages(); + } + + public void Dispose() { } + } +} diff --git a/src/Ryujinx.Graphics.Metal/IndexBufferPattern.cs b/src/Ryujinx.Graphics.Metal/IndexBufferPattern.cs new file mode 100644 index 000000000..24e3222fe --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/IndexBufferPattern.cs @@ -0,0 +1,118 @@ +using Ryujinx.Graphics.GAL; +using System; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + internal class IndexBufferPattern : IDisposable + { + public int PrimitiveVertices { get; } + public int PrimitiveVerticesOut { get; } + public int BaseIndex { get; } + public int[] OffsetIndex { get; } + public int IndexStride { get; } + public bool RepeatStart { get; } + + private readonly MetalRenderer _renderer; + private int _currentSize; + private BufferHandle _repeatingBuffer; + + public IndexBufferPattern(MetalRenderer renderer, + int primitiveVertices, + int primitiveVerticesOut, + int baseIndex, + int[] offsetIndex, + int indexStride, + bool repeatStart) + { + PrimitiveVertices = primitiveVertices; + PrimitiveVerticesOut = primitiveVerticesOut; + BaseIndex = baseIndex; + OffsetIndex = offsetIndex; + IndexStride = indexStride; + RepeatStart = repeatStart; + + _renderer = renderer; + } + + public int GetPrimitiveCount(int vertexCount) + { + return Math.Max(0, (vertexCount - BaseIndex) / IndexStride); + } + + public int GetConvertedCount(int indexCount) + { + int primitiveCount = GetPrimitiveCount(indexCount); + return primitiveCount * OffsetIndex.Length; + } + + public BufferHandle GetRepeatingBuffer(int vertexCount, out int indexCount) + { + int primitiveCount = GetPrimitiveCount(vertexCount); + indexCount = primitiveCount * PrimitiveVerticesOut; + + int expectedSize = primitiveCount * OffsetIndex.Length; + + if (expectedSize <= _currentSize && _repeatingBuffer != BufferHandle.Null) + { + return _repeatingBuffer; + } + + // Expand the repeating pattern to the number of requested primitives. + BufferHandle newBuffer = _renderer.BufferManager.CreateWithHandle(expectedSize * sizeof(int)); + + // Copy the old data to the new one. + if (_repeatingBuffer != BufferHandle.Null) + { + _renderer.Pipeline.CopyBuffer(_repeatingBuffer, newBuffer, 0, 0, _currentSize * sizeof(int)); + _renderer.BufferManager.Delete(_repeatingBuffer); + } + + _repeatingBuffer = newBuffer; + + // Add the additional repeats on top. + int newPrimitives = primitiveCount; + int oldPrimitives = (_currentSize) / OffsetIndex.Length; + + int[] newData; + + newPrimitives -= oldPrimitives; + newData = new int[expectedSize - _currentSize]; + + int outOffset = 0; + int index = oldPrimitives * IndexStride + BaseIndex; + + for (int i = 0; i < newPrimitives; i++) + { + if (RepeatStart) + { + // Used for triangle fan + newData[outOffset++] = 0; + } + + for (int j = RepeatStart ? 1 : 0; j < OffsetIndex.Length; j++) + { + newData[outOffset++] = index + OffsetIndex[j]; + } + + index += IndexStride; + } + + _renderer.SetBufferData(newBuffer, _currentSize * sizeof(int), MemoryMarshal.Cast(newData)); + _currentSize = expectedSize; + + return newBuffer; + } + + public void Dispose() + { + if (_repeatingBuffer != BufferHandle.Null) + { + _renderer.BufferManager.Delete(_repeatingBuffer); + _repeatingBuffer = BufferHandle.Null; + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/IndexBufferState.cs b/src/Ryujinx.Graphics.Metal/IndexBufferState.cs new file mode 100644 index 000000000..411df9685 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/IndexBufferState.cs @@ -0,0 +1,103 @@ +using Ryujinx.Graphics.GAL; +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + readonly internal struct IndexBufferState + { + public static IndexBufferState Null => new(BufferHandle.Null, 0, 0); + + private readonly int _offset; + private readonly int _size; + private readonly IndexType _type; + + private readonly BufferHandle _handle; + + public IndexBufferState(BufferHandle handle, int offset, int size, IndexType type = IndexType.UInt) + { + _handle = handle; + _offset = offset; + _size = size; + _type = type; + } + + public (MTLBuffer, int, MTLIndexType) GetIndexBuffer(MetalRenderer renderer, CommandBufferScoped cbs) + { + Auto autoBuffer; + int offset, size; + MTLIndexType type; + + if (_type == IndexType.UByte) + { + // Index type is not supported. Convert to I16. + autoBuffer = renderer.BufferManager.GetBufferI8ToI16(cbs, _handle, _offset, _size); + + type = MTLIndexType.UInt16; + offset = 0; + size = _size * 2; + } + else + { + autoBuffer = renderer.BufferManager.GetBuffer(_handle, false, out int bufferSize); + + if (_offset >= bufferSize) + { + autoBuffer = null; + } + + type = _type.Convert(); + offset = _offset; + size = _size; + } + + if (autoBuffer != null) + { + DisposableBuffer buffer = autoBuffer.Get(cbs, offset, size); + + return (buffer.Value, offset, type); + } + + return (new MTLBuffer(IntPtr.Zero), 0, MTLIndexType.UInt16); + } + + public (MTLBuffer, int, MTLIndexType) GetConvertedIndexBuffer( + MetalRenderer renderer, + CommandBufferScoped cbs, + int firstIndex, + int indexCount, + int convertedCount, + IndexBufferPattern pattern) + { + // Convert the index buffer using the given pattern. + int indexSize = GetIndexSize(); + + int firstIndexOffset = firstIndex * indexSize; + + var autoBuffer = renderer.BufferManager.GetBufferTopologyConversion(cbs, _handle, _offset + firstIndexOffset, indexCount * indexSize, pattern, indexSize); + + int size = convertedCount * 4; + + if (autoBuffer != null) + { + DisposableBuffer buffer = autoBuffer.Get(cbs, 0, size); + + return (buffer.Value, 0, MTLIndexType.UInt32); + } + + return (new MTLBuffer(IntPtr.Zero), 0, MTLIndexType.UInt32); + } + + private int GetIndexSize() + { + return _type switch + { + IndexType.UInt => 4, + IndexType.UShort => 2, + _ => 1, + }; + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/MetalRenderer.cs b/src/Ryujinx.Graphics.Metal/MetalRenderer.cs new file mode 100644 index 000000000..935a5b3b1 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/MetalRenderer.cs @@ -0,0 +1,309 @@ +using Ryujinx.Common.Configuration; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Shader.Translation; +using SharpMetal.Metal; +using SharpMetal.QuartzCore; +using System; +using System.Collections.Generic; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + public sealed class MetalRenderer : IRenderer + { + public const int TotalSets = 4; + + private readonly MTLDevice _device; + private readonly MTLCommandQueue _queue; + private readonly Func _getMetalLayer; + + private Pipeline _pipeline; + private Window _window; + + public uint ProgramCount { get; set; } = 0; + + public event EventHandler ScreenCaptured; + public bool PreferThreading => true; + public IPipeline Pipeline => _pipeline; + public IWindow Window => _window; + + internal MTLCommandQueue BackgroundQueue { get; private set; } + internal HelperShader HelperShader { get; private set; } + internal BufferManager BufferManager { get; private set; } + internal CommandBufferPool CommandBufferPool { get; private set; } + internal BackgroundResources BackgroundResources { get; private set; } + internal Action InterruptAction { get; private set; } + internal SyncManager SyncManager { get; private set; } + + internal HashSet Programs { get; } + internal HashSet Samplers { get; } + + public MetalRenderer(Func metalLayer) + { + _device = MTLDevice.CreateSystemDefaultDevice(); + Programs = new HashSet(); + Samplers = new HashSet(); + + if (_device.ArgumentBuffersSupport != MTLArgumentBuffersTier.Tier2) + { + throw new NotSupportedException("Metal backend requires Tier 2 Argument Buffer support."); + } + + _queue = _device.NewCommandQueue(CommandBufferPool.MaxCommandBuffers + 1); + BackgroundQueue = _device.NewCommandQueue(CommandBufferPool.MaxCommandBuffers); + + _getMetalLayer = metalLayer; + } + + public void Initialize(GraphicsDebugLevel logLevel) + { + var layer = _getMetalLayer(); + layer.Device = _device; + layer.FramebufferOnly = false; + + CommandBufferPool = new CommandBufferPool(_queue); + _window = new Window(this, layer); + _pipeline = new Pipeline(_device, this); + BufferManager = new BufferManager(_device, this, _pipeline); + + _pipeline.InitEncoderStateManager(BufferManager); + + BackgroundResources = new BackgroundResources(this); + HelperShader = new HelperShader(_device, this, _pipeline); + SyncManager = new SyncManager(this); + } + + public void BackgroundContextAction(Action action, bool alwaysBackground = false) + { + // GetData methods should be thread safe, so we can call this directly. + // Texture copy (scaled) may also happen in here, so that should also be thread safe. + + action(); + } + + public BufferHandle CreateBuffer(int size, BufferAccess access) + { + return BufferManager.CreateWithHandle(size); + } + + public BufferHandle CreateBuffer(IntPtr pointer, int size) + { + return BufferManager.Create(pointer, size); + } + + public BufferHandle CreateBufferSparse(ReadOnlySpan storageBuffers) + { + throw new NotImplementedException(); + } + + public IImageArray CreateImageArray(int size, bool isBuffer) + { + return new ImageArray(size, isBuffer, _pipeline); + } + + public IProgram CreateProgram(ShaderSource[] shaders, ShaderInfo info) + { + ProgramCount++; + return new Program(this, _device, shaders, info.ResourceLayout, info.ComputeLocalSize); + } + + public ISampler CreateSampler(SamplerCreateInfo info) + { + return new SamplerHolder(this, _device, info); + } + + public ITexture CreateTexture(TextureCreateInfo info) + { + if (info.Target == Target.TextureBuffer) + { + return new TextureBuffer(_device, this, _pipeline, info); + } + + return new Texture(_device, this, _pipeline, info); + } + + public ITextureArray CreateTextureArray(int size, bool isBuffer) + { + return new TextureArray(size, isBuffer, _pipeline); + } + + public bool PrepareHostMapping(IntPtr address, ulong size) + { + // TODO: Metal Host Mapping + return false; + } + + public void CreateSync(ulong id, bool strict) + { + SyncManager.Create(id, strict); + } + + public void DeleteBuffer(BufferHandle buffer) + { + BufferManager.Delete(buffer); + } + + public PinnedSpan GetBufferData(BufferHandle buffer, int offset, int size) + { + return BufferManager.GetData(buffer, offset, size); + } + + public Capabilities GetCapabilities() + { + // TODO: Finalize these values + return new Capabilities( + api: TargetApi.Metal, + vendorName: HardwareInfoTools.GetVendor(), + SystemMemoryType.UnifiedMemory, + hasFrontFacingBug: false, + hasVectorIndexingBug: false, + needsFragmentOutputSpecialization: true, + reduceShaderPrecision: true, + supportsAstcCompression: true, + supportsBc123Compression: true, + supportsBc45Compression: true, + supportsBc67Compression: true, + supportsEtc2Compression: true, + supports3DTextureCompression: true, + supportsBgraFormat: true, + supportsR4G4Format: false, + supportsR4G4B4A4Format: true, + supportsScaledVertexFormats: false, + supportsSnormBufferTextureFormat: true, + supportsSparseBuffer: false, + supports5BitComponentFormat: true, + supportsBlendEquationAdvanced: false, + supportsFragmentShaderInterlock: true, + supportsFragmentShaderOrderingIntel: false, + supportsGeometryShader: false, + supportsGeometryShaderPassthrough: false, + supportsTransformFeedback: false, + supportsImageLoadFormatted: false, + supportsLayerVertexTessellation: false, + supportsMismatchingViewFormat: true, + supportsCubemapView: true, + supportsNonConstantTextureOffset: false, + supportsQuads: false, + supportsSeparateSampler: true, + supportsShaderBallot: false, + supportsShaderBarrierDivergence: false, + supportsShaderFloat64: false, + supportsTextureGatherOffsets: false, + supportsTextureShadowLod: false, + supportsVertexStoreAndAtomics: false, + supportsViewportIndexVertexTessellation: false, + supportsViewportMask: false, + supportsViewportSwizzle: false, + supportsIndirectParameters: true, + supportsDepthClipControl: false, + uniformBufferSetIndex: (int)Constants.ConstantBuffersSetIndex, + storageBufferSetIndex: (int)Constants.StorageBuffersSetIndex, + textureSetIndex: (int)Constants.TexturesSetIndex, + imageSetIndex: (int)Constants.ImagesSetIndex, + extraSetBaseIndex: TotalSets, + maximumExtraSets: (int)Constants.MaximumExtraSets, + maximumUniformBuffersPerStage: Constants.MaxUniformBuffersPerStage, + maximumStorageBuffersPerStage: Constants.MaxStorageBuffersPerStage, + maximumTexturesPerStage: Constants.MaxTexturesPerStage, + maximumImagesPerStage: Constants.MaxImagesPerStage, + maximumComputeSharedMemorySize: (int)_device.MaxThreadgroupMemoryLength, + maximumSupportedAnisotropy: 16, + shaderSubgroupSize: 256, + storageBufferOffsetAlignment: 16, + textureBufferOffsetAlignment: 16, + gatherBiasPrecision: 0, + maximumGpuMemory: 0 + ); + } + + public ulong GetCurrentSync() + { + return SyncManager.GetCurrent(); + } + + public HardwareInfo GetHardwareInfo() + { + return new HardwareInfo(HardwareInfoTools.GetVendor(), HardwareInfoTools.GetModel(), "Apple"); + } + + public IProgram LoadProgramBinary(byte[] programBinary, bool hasFragmentShader, ShaderInfo info) + { + throw new NotImplementedException(); + } + + public void SetBufferData(BufferHandle buffer, int offset, ReadOnlySpan data) + { + BufferManager.SetData(buffer, offset, data, _pipeline.Cbs); + } + + public void UpdateCounters() + { + // https://developer.apple.com/documentation/metal/gpu_counters_and_counter_sample_buffers/creating_a_counter_sample_buffer_to_store_a_gpu_s_counter_data_during_a_pass?language=objc + } + + public void PreFrame() + { + SyncManager.Cleanup(); + } + + public ICounterEvent ReportCounter(CounterType type, EventHandler resultHandler, float divisor, bool hostReserved) + { + // https://developer.apple.com/documentation/metal/gpu_counters_and_counter_sample_buffers/creating_a_counter_sample_buffer_to_store_a_gpu_s_counter_data_during_a_pass?language=objc + var counterEvent = new CounterEvent(); + resultHandler?.Invoke(counterEvent, type == CounterType.SamplesPassed ? (ulong)1 : 0); + return counterEvent; + } + + public void ResetCounter(CounterType type) + { + // https://developer.apple.com/documentation/metal/gpu_counters_and_counter_sample_buffers/creating_a_counter_sample_buffer_to_store_a_gpu_s_counter_data_during_a_pass?language=objc + } + + public void WaitSync(ulong id) + { + SyncManager.Wait(id); + } + + public void FlushAllCommands() + { + _pipeline.FlushCommandsImpl(); + } + + public void RegisterFlush() + { + SyncManager.RegisterFlush(); + + // Periodically free unused regions of the staging buffer to avoid doing it all at once. + BufferManager.StagingBuffer.FreeCompleted(); + } + + public void SetInterruptAction(Action interruptAction) + { + InterruptAction = interruptAction; + } + + public void Screenshot() + { + // TODO: Screenshots + } + + public void Dispose() + { + BackgroundResources.Dispose(); + + foreach (var program in Programs) + { + program.Dispose(); + } + + foreach (var sampler in Samplers) + { + sampler.Dispose(); + } + + _pipeline.Dispose(); + _window.Dispose(); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/MultiFenceHolder.cs b/src/Ryujinx.Graphics.Metal/MultiFenceHolder.cs new file mode 100644 index 000000000..cd5ad08ba --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/MultiFenceHolder.cs @@ -0,0 +1,262 @@ +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + /// + /// Holder for multiple host GPU fences. + /// + [SupportedOSPlatform("macos")] + class MultiFenceHolder + { + private const int BufferUsageTrackingGranularity = 4096; + + private readonly FenceHolder[] _fences; + private readonly BufferUsageBitmap _bufferUsageBitmap; + + /// + /// Creates a new instance of the multiple fence holder. + /// + public MultiFenceHolder() + { + _fences = new FenceHolder[CommandBufferPool.MaxCommandBuffers]; + } + + /// + /// Creates a new instance of the multiple fence holder, with a given buffer size in mind. + /// + /// Size of the buffer + public MultiFenceHolder(int size) + { + _fences = new FenceHolder[CommandBufferPool.MaxCommandBuffers]; + _bufferUsageBitmap = new BufferUsageBitmap(size, BufferUsageTrackingGranularity); + } + + /// + /// Adds read/write buffer usage information to the uses list. + /// + /// Index of the command buffer where the buffer is used + /// Offset of the buffer being used + /// Size of the buffer region being used, in bytes + /// Whether the access is a write or not + public void AddBufferUse(int cbIndex, int offset, int size, bool write) + { + _bufferUsageBitmap.Add(cbIndex, offset, size, false); + + if (write) + { + _bufferUsageBitmap.Add(cbIndex, offset, size, true); + } + } + + /// + /// Removes all buffer usage information for a given command buffer. + /// + /// Index of the command buffer where the buffer is used + public void RemoveBufferUses(int cbIndex) + { + _bufferUsageBitmap?.Clear(cbIndex); + } + + /// + /// Checks if a given range of a buffer is being used by a command buffer still being processed by the GPU. + /// + /// Index of the command buffer where the buffer is used + /// Offset of the buffer being used + /// Size of the buffer region being used, in bytes + /// True if in use, false otherwise + public bool IsBufferRangeInUse(int cbIndex, int offset, int size) + { + return _bufferUsageBitmap.OverlapsWith(cbIndex, offset, size); + } + + /// + /// Checks if a given range of a buffer is being used by any command buffer still being processed by the GPU. + /// + /// Offset of the buffer being used + /// Size of the buffer region being used, in bytes + /// True if only write usages should count + /// True if in use, false otherwise + public bool IsBufferRangeInUse(int offset, int size, bool write) + { + return _bufferUsageBitmap.OverlapsWith(offset, size, write); + } + + /// + /// Adds a fence to the holder. + /// + /// Command buffer index of the command buffer that owns the fence + /// Fence to be added + /// True if the command buffer's previous fence value was null + public bool AddFence(int cbIndex, FenceHolder fence) + { + ref FenceHolder fenceRef = ref _fences[cbIndex]; + + if (fenceRef == null) + { + fenceRef = fence; + return true; + } + + return false; + } + + /// + /// Removes a fence from the holder. + /// + /// Command buffer index of the command buffer that owns the fence + public void RemoveFence(int cbIndex) + { + _fences[cbIndex] = null; + } + + /// + /// Determines if a fence referenced on the given command buffer. + /// + /// Index of the command buffer to check if it's used + /// True if referenced, false otherwise + public bool HasFence(int cbIndex) + { + return _fences[cbIndex] != null; + } + + /// + /// Wait until all the fences on the holder are signaled. + /// + public void WaitForFences() + { + WaitForFencesImpl(0, 0, true); + } + + /// + /// Wait until all the fences on the holder with buffer uses overlapping the specified range are signaled. + /// + /// Start offset of the buffer range + /// Size of the buffer range in bytes + public void WaitForFences(int offset, int size) + { + WaitForFencesImpl(offset, size, true); + } + + /// + /// Wait until all the fences on the holder with buffer uses overlapping the specified range are signaled. + /// + + // TODO: Add a proper timeout! + public bool WaitForFences(bool indefinite) + { + return WaitForFencesImpl(0, 0, indefinite); + } + + /// + /// Wait until all the fences on the holder with buffer uses overlapping the specified range are signaled. + /// + /// Start offset of the buffer range + /// Size of the buffer range in bytes + /// Indicates if this should wait indefinitely + /// True if all fences were signaled before the timeout expired, false otherwise + private bool WaitForFencesImpl(int offset, int size, bool indefinite) + { + Span fenceHolders = new FenceHolder[CommandBufferPool.MaxCommandBuffers]; + + int count = size != 0 ? GetOverlappingFences(fenceHolders, offset, size) : GetFences(fenceHolders); + Span fences = stackalloc MTLCommandBuffer[count]; + + int fenceCount = 0; + + for (int i = 0; i < count; i++) + { + if (fenceHolders[i].TryGet(out MTLCommandBuffer fence)) + { + fences[fenceCount] = fence; + + if (fenceCount < i) + { + fenceHolders[fenceCount] = fenceHolders[i]; + } + + fenceCount++; + } + } + + if (fenceCount == 0) + { + return true; + } + + bool signaled = true; + + if (indefinite) + { + foreach (var fence in fences) + { + fence.WaitUntilCompleted(); + } + } + else + { + foreach (var fence in fences) + { + if (fence.Status != MTLCommandBufferStatus.Completed) + { + signaled = false; + } + } + } + + for (int i = 0; i < fenceCount; i++) + { + fenceHolders[i].Put(); + } + + return signaled; + } + + /// + /// Gets fences to wait for. + /// + /// Span to store fences in + /// Number of fences placed in storage + private int GetFences(Span storage) + { + int count = 0; + + for (int i = 0; i < _fences.Length; i++) + { + var fence = _fences[i]; + + if (fence != null) + { + storage[count++] = fence; + } + } + + return count; + } + + /// + /// Gets fences to wait for use of a given buffer region. + /// + /// Span to store overlapping fences in + /// Offset of the range + /// Size of the range in bytes + /// Number of fences for the specified region placed in storage + private int GetOverlappingFences(Span storage, int offset, int size) + { + int count = 0; + + for (int i = 0; i < _fences.Length; i++) + { + var fence = _fences[i]; + + if (fence != null && _bufferUsageBitmap.OverlapsWith(i, offset, size)) + { + storage[count++] = fence; + } + } + + return count; + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/PersistentFlushBuffer.cs b/src/Ryujinx.Graphics.Metal/PersistentFlushBuffer.cs new file mode 100644 index 000000000..fa3df47db --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/PersistentFlushBuffer.cs @@ -0,0 +1,99 @@ +using Ryujinx.Graphics.GAL; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + internal class PersistentFlushBuffer : IDisposable + { + private readonly MetalRenderer _renderer; + + private BufferHolder _flushStorage; + + public PersistentFlushBuffer(MetalRenderer renderer) + { + _renderer = renderer; + } + + private BufferHolder ResizeIfNeeded(int size) + { + var flushStorage = _flushStorage; + + if (flushStorage == null || size > _flushStorage.Size) + { + flushStorage?.Dispose(); + + flushStorage = _renderer.BufferManager.Create(size); + _flushStorage = flushStorage; + } + + return flushStorage; + } + + public Span GetBufferData(CommandBufferPool cbp, BufferHolder buffer, int offset, int size) + { + var flushStorage = ResizeIfNeeded(size); + Auto srcBuffer; + + using (var cbs = cbp.Rent()) + { + srcBuffer = buffer.GetBuffer(); + var dstBuffer = flushStorage.GetBuffer(); + + if (srcBuffer.TryIncrementReferenceCount()) + { + BufferHolder.Copy(cbs, srcBuffer, dstBuffer, offset, 0, size, registerSrcUsage: false); + } + else + { + // Source buffer is no longer alive, don't copy anything to flush storage. + srcBuffer = null; + } + } + + flushStorage.WaitForFences(); + srcBuffer?.DecrementReferenceCount(); + return flushStorage.GetDataStorage(0, size); + } + + public Span GetTextureData(CommandBufferPool cbp, Texture view, int size) + { + TextureCreateInfo info = view.Info; + + var flushStorage = ResizeIfNeeded(size); + + using (var cbs = cbp.Rent()) + { + var buffer = flushStorage.GetBuffer().Get(cbs).Value; + var image = view.GetHandle(); + + view.CopyFromOrToBuffer(cbs, buffer, image, size, true, 0, 0, info.GetLayers(), info.Levels, singleSlice: false); + } + + flushStorage.WaitForFences(); + return flushStorage.GetDataStorage(0, size); + } + + public Span GetTextureData(CommandBufferPool cbp, Texture view, int size, int layer, int level) + { + var flushStorage = ResizeIfNeeded(size); + + using (var cbs = cbp.Rent()) + { + var buffer = flushStorage.GetBuffer().Get(cbs).Value; + var image = view.GetHandle(); + + view.CopyFromOrToBuffer(cbs, buffer, image, size, true, layer, level, 1, 1, singleSlice: true); + } + + flushStorage.WaitForFences(); + return flushStorage.GetDataStorage(0, size); + } + + public void Dispose() + { + _flushStorage.Dispose(); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/Pipeline.cs b/src/Ryujinx.Graphics.Metal/Pipeline.cs new file mode 100644 index 000000000..113974061 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Pipeline.cs @@ -0,0 +1,877 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Shader; +using SharpMetal.Foundation; +using SharpMetal.Metal; +using SharpMetal.QuartzCore; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + public enum EncoderType + { + Blit, + Compute, + Render, + None + } + + [SupportedOSPlatform("macos")] + class Pipeline : IPipeline, IEncoderFactory, IDisposable + { + private const ulong MinByteWeightForFlush = 256 * 1024 * 1024; // MiB + + private readonly MTLDevice _device; + private readonly MetalRenderer _renderer; + private EncoderStateManager _encoderStateManager; + private ulong _byteWeight; + + public MTLCommandBuffer CommandBuffer; + + public IndexBufferPattern QuadsToTrisPattern; + public IndexBufferPattern TriFanToTrisPattern; + + internal CommandBufferScoped? PreloadCbs { get; private set; } + internal CommandBufferScoped Cbs { get; private set; } + internal CommandBufferEncoder Encoders => Cbs.Encoders; + internal EncoderType CurrentEncoderType => Encoders.CurrentEncoderType; + + public Pipeline(MTLDevice device, MetalRenderer renderer) + { + _device = device; + _renderer = renderer; + + renderer.CommandBufferPool.Initialize(this); + + CommandBuffer = (Cbs = _renderer.CommandBufferPool.Rent()).CommandBuffer; + } + + internal void InitEncoderStateManager(BufferManager bufferManager) + { + _encoderStateManager = new EncoderStateManager(_device, bufferManager, this); + + QuadsToTrisPattern = new IndexBufferPattern(_renderer, 4, 6, 0, [0, 1, 2, 0, 2, 3], 4, false); + TriFanToTrisPattern = new IndexBufferPattern(_renderer, 3, 3, 2, [int.MinValue, -1, 0], 1, true); + } + + public EncoderState SwapState(EncoderState state, DirtyFlags flags = DirtyFlags.All, bool endRenderPass = true) + { + if (endRenderPass && CurrentEncoderType == EncoderType.Render) + { + EndCurrentPass(); + } + + return _encoderStateManager.SwapState(state, flags); + } + + public PredrawState SavePredrawState() + { + return _encoderStateManager.SavePredrawState(); + } + + public void RestorePredrawState(PredrawState state) + { + _encoderStateManager.RestorePredrawState(state); + } + + public void SetClearLoadAction(bool clear) + { + _encoderStateManager.SetClearLoadAction(clear); + } + + public MTLRenderCommandEncoder GetOrCreateRenderEncoder(bool forDraw = false) + { + // Mark all state as dirty to ensure it is set on the new encoder + if (Cbs.Encoders.CurrentEncoderType != EncoderType.Render) + { + _encoderStateManager.SignalRenderDirty(); + } + + if (forDraw) + { + _encoderStateManager.RenderResourcesPrepass(); + } + + MTLRenderCommandEncoder renderCommandEncoder = Cbs.Encoders.EnsureRenderEncoder(); + + if (forDraw) + { + _encoderStateManager.RebindRenderState(renderCommandEncoder); + } + + return renderCommandEncoder; + } + + public MTLBlitCommandEncoder GetOrCreateBlitEncoder() + { + return Cbs.Encoders.EnsureBlitEncoder(); + } + + public MTLComputeCommandEncoder GetOrCreateComputeEncoder(bool forDispatch = false) + { + // Mark all state as dirty to ensure it is set on the new encoder + if (Cbs.Encoders.CurrentEncoderType != EncoderType.Compute) + { + _encoderStateManager.SignalComputeDirty(); + } + + if (forDispatch) + { + _encoderStateManager.ComputeResourcesPrepass(); + } + + MTLComputeCommandEncoder computeCommandEncoder = Cbs.Encoders.EnsureComputeEncoder(); + + if (forDispatch) + { + _encoderStateManager.RebindComputeState(computeCommandEncoder); + } + + return computeCommandEncoder; + } + + public void EndCurrentPass() + { + Cbs.Encoders.EndCurrentPass(); + } + + public MTLRenderCommandEncoder CreateRenderCommandEncoder() + { + return _encoderStateManager.CreateRenderCommandEncoder(); + } + + public MTLComputeCommandEncoder CreateComputeCommandEncoder() + { + return _encoderStateManager.CreateComputeCommandEncoder(); + } + + public void Present(CAMetalDrawable drawable, Texture src, Extents2D srcRegion, Extents2D dstRegion, bool isLinear) + { + // TODO: Clean this up + var textureInfo = new TextureCreateInfo((int)drawable.Texture.Width, (int)drawable.Texture.Height, (int)drawable.Texture.Depth, (int)drawable.Texture.MipmapLevelCount, (int)drawable.Texture.SampleCount, 0, 0, 0, Format.B8G8R8A8Unorm, 0, Target.Texture2D, SwizzleComponent.Red, SwizzleComponent.Green, SwizzleComponent.Blue, SwizzleComponent.Alpha); + var dst = new Texture(_device, _renderer, this, textureInfo, drawable.Texture, 0, 0); + + _renderer.HelperShader.BlitColor(Cbs, src, dst, srcRegion, dstRegion, isLinear, true); + + EndCurrentPass(); + + Cbs.CommandBuffer.PresentDrawable(drawable); + + FlushCommandsImpl(); + + // TODO: Auto flush counting + _renderer.SyncManager.GetAndResetWaitTicks(); + + // Cleanup + dst.Dispose(); + } + + public CommandBufferScoped GetPreloadCommandBuffer() + { + PreloadCbs ??= _renderer.CommandBufferPool.Rent(); + + return PreloadCbs.Value; + } + + public void FlushCommandsIfWeightExceeding(IAuto disposedResource, ulong byteWeight) + { + bool usedByCurrentCb = disposedResource.HasCommandBufferDependency(Cbs); + + if (PreloadCbs != null && !usedByCurrentCb) + { + usedByCurrentCb = disposedResource.HasCommandBufferDependency(PreloadCbs.Value); + } + + if (usedByCurrentCb) + { + // Since we can only free memory after the command buffer that uses a given resource was executed, + // keeping the command buffer might cause a high amount of memory to be in use. + // To prevent that, we force submit command buffers if the memory usage by resources + // in use by the current command buffer is above a given limit, and those resources were disposed. + _byteWeight += byteWeight; + + if (_byteWeight >= MinByteWeightForFlush) + { + FlushCommandsImpl(); + } + } + } + + public void FlushCommandsImpl() + { + EndCurrentPass(); + + _byteWeight = 0; + + if (PreloadCbs != null) + { + PreloadCbs.Value.Dispose(); + PreloadCbs = null; + } + + CommandBuffer = (Cbs = _renderer.CommandBufferPool.ReturnAndRent(Cbs)).CommandBuffer; + _renderer.RegisterFlush(); + } + + public void DirtyTextures() + { + _encoderStateManager.DirtyTextures(); + } + + public void DirtyImages() + { + _encoderStateManager.DirtyImages(); + } + + public void Blit( + Texture src, + Texture dst, + Extents2D srcRegion, + Extents2D dstRegion, + bool isDepthOrStencil, + bool linearFilter) + { + if (isDepthOrStencil) + { + _renderer.HelperShader.BlitDepthStencil(Cbs, src, dst, srcRegion, dstRegion); + } + else + { + _renderer.HelperShader.BlitColor(Cbs, src, dst, srcRegion, dstRegion, linearFilter); + } + } + + public void Barrier() + { + switch (CurrentEncoderType) + { + case EncoderType.Render: + { + var scope = MTLBarrierScope.Buffers | MTLBarrierScope.Textures | MTLBarrierScope.RenderTargets; + MTLRenderStages stages = MTLRenderStages.RenderStageVertex | MTLRenderStages.RenderStageFragment; + Encoders.RenderEncoder.MemoryBarrier(scope, stages, stages); + break; + } + case EncoderType.Compute: + { + var scope = MTLBarrierScope.Buffers | MTLBarrierScope.Textures | MTLBarrierScope.RenderTargets; + Encoders.ComputeEncoder.MemoryBarrier(scope); + break; + } + } + } + + public void ClearBuffer(BufferHandle destination, int offset, int size, uint value) + { + var blitCommandEncoder = GetOrCreateBlitEncoder(); + + var mtlBuffer = _renderer.BufferManager.GetBuffer(destination, offset, size, true).Get(Cbs, offset, size, true).Value; + + // Might need a closer look, range's count, lower, and upper bound + // must be a multiple of 4 + blitCommandEncoder.FillBuffer(mtlBuffer, + new NSRange + { + location = (ulong)offset, + length = (ulong)size + }, + (byte)value); + } + + public void ClearRenderTargetColor(int index, int layer, int layerCount, uint componentMask, ColorF color) + { + float[] colors = [color.Red, color.Green, color.Blue, color.Alpha]; + var dst = _encoderStateManager.RenderTargets[index]; + + // TODO: Remove workaround for Wonder which has an invalid texture due to unsupported format + if (dst == null) + { + Logger.Warning?.PrintMsg(LogClass.Gpu, "Attempted to clear invalid render target!"); + return; + } + + _renderer.HelperShader.ClearColor(index, colors, componentMask, dst.Width, dst.Height, dst.Info.Format); + } + + public void ClearRenderTargetDepthStencil(int layer, int layerCount, float depthValue, bool depthMask, int stencilValue, int stencilMask) + { + var depthStencil = _encoderStateManager.DepthStencil; + + if (depthStencil == null) + { + return; + } + + _renderer.HelperShader.ClearDepthStencil(depthValue, depthMask, stencilValue, stencilMask, depthStencil.Width, depthStencil.Height); + } + + public void CommandBufferBarrier() + { + Barrier(); + } + + public void CopyBuffer(BufferHandle src, BufferHandle dst, int srcOffset, int dstOffset, int size) + { + var srcBuffer = _renderer.BufferManager.GetBuffer(src, srcOffset, size, false); + var dstBuffer = _renderer.BufferManager.GetBuffer(dst, dstOffset, size, true); + + BufferHolder.Copy(Cbs, srcBuffer, dstBuffer, srcOffset, dstOffset, size); + } + + public void PushDebugGroup(string name) + { + var encoder = Encoders.CurrentEncoder; + var debugGroupName = StringHelper.NSString(name); + + if (encoder == null) + { + return; + } + + switch (Encoders.CurrentEncoderType) + { + case EncoderType.Render: + encoder.Value.PushDebugGroup(debugGroupName); + break; + case EncoderType.Blit: + encoder.Value.PushDebugGroup(debugGroupName); + break; + case EncoderType.Compute: + encoder.Value.PushDebugGroup(debugGroupName); + break; + } + } + + public void PopDebugGroup() + { + var encoder = Encoders.CurrentEncoder; + + if (encoder == null) + { + return; + } + + switch (Encoders.CurrentEncoderType) + { + case EncoderType.Render: + encoder.Value.PopDebugGroup(); + break; + case EncoderType.Blit: + encoder.Value.PopDebugGroup(); + break; + case EncoderType.Compute: + encoder.Value.PopDebugGroup(); + break; + } + } + + public void DispatchCompute(int groupsX, int groupsY, int groupsZ) + { + DispatchCompute(groupsX, groupsY, groupsZ, String.Empty); + } + + public void DispatchCompute(int groupsX, int groupsY, int groupsZ, string debugGroupName) + { + var computeCommandEncoder = GetOrCreateComputeEncoder(true); + + ComputeSize localSize = _encoderStateManager.ComputeLocalSize; + + if (debugGroupName != String.Empty) + { + PushDebugGroup(debugGroupName); + } + + computeCommandEncoder.DispatchThreadgroups( + new MTLSize { width = (ulong)groupsX, height = (ulong)groupsY, depth = (ulong)groupsZ }, + new MTLSize { width = (ulong)localSize.X, height = (ulong)localSize.Y, depth = (ulong)localSize.Z }); + + if (debugGroupName != String.Empty) + { + PopDebugGroup(); + } + } + + public void Draw(int vertexCount, int instanceCount, int firstVertex, int firstInstance) + { + Draw(vertexCount, instanceCount, firstVertex, firstInstance, String.Empty); + } + + public void Draw(int vertexCount, int instanceCount, int firstVertex, int firstInstance, string debugGroupName) + { + if (vertexCount == 0) + { + return; + } + + var primitiveType = TopologyRemap(_encoderStateManager.Topology).Convert(); + + if (TopologyUnsupported(_encoderStateManager.Topology)) + { + var pattern = GetIndexBufferPattern(); + + BufferHandle handle = pattern.GetRepeatingBuffer(vertexCount, out int indexCount); + var buffer = _renderer.BufferManager.GetBuffer(handle, false); + var mtlBuffer = buffer.Get(Cbs, 0, indexCount * sizeof(int)).Value; + + var renderCommandEncoder = GetOrCreateRenderEncoder(true); + + renderCommandEncoder.DrawIndexedPrimitives( + primitiveType, + (ulong)indexCount, + MTLIndexType.UInt32, + mtlBuffer, + 0); + } + else + { + var renderCommandEncoder = GetOrCreateRenderEncoder(true); + + if (debugGroupName != String.Empty) + { + PushDebugGroup(debugGroupName); + } + + renderCommandEncoder.DrawPrimitives( + primitiveType, + (ulong)firstVertex, + (ulong)vertexCount, + (ulong)instanceCount, + (ulong)firstInstance); + + if (debugGroupName != String.Empty) + { + PopDebugGroup(); + } + } + } + + private IndexBufferPattern GetIndexBufferPattern() + { + return _encoderStateManager.Topology switch + { + PrimitiveTopology.Quads => QuadsToTrisPattern, + PrimitiveTopology.TriangleFan or PrimitiveTopology.Polygon => TriFanToTrisPattern, + _ => throw new NotSupportedException($"Unsupported topology: {_encoderStateManager.Topology}"), + }; + } + + private PrimitiveTopology TopologyRemap(PrimitiveTopology topology) + { + return topology switch + { + PrimitiveTopology.Quads => PrimitiveTopology.Triangles, + PrimitiveTopology.QuadStrip => PrimitiveTopology.TriangleStrip, + PrimitiveTopology.TriangleFan or PrimitiveTopology.Polygon => PrimitiveTopology.Triangles, + _ => topology, + }; + } + + private bool TopologyUnsupported(PrimitiveTopology topology) + { + return topology switch + { + PrimitiveTopology.Quads or PrimitiveTopology.TriangleFan or PrimitiveTopology.Polygon => true, + _ => false, + }; + } + + public void DrawIndexed(int indexCount, int instanceCount, int firstIndex, int firstVertex, int firstInstance) + { + if (indexCount == 0) + { + return; + } + + MTLBuffer mtlBuffer; + int offset; + MTLIndexType type; + int finalIndexCount = indexCount; + + var primitiveType = TopologyRemap(_encoderStateManager.Topology).Convert(); + + if (TopologyUnsupported(_encoderStateManager.Topology)) + { + var pattern = GetIndexBufferPattern(); + int convertedCount = pattern.GetConvertedCount(indexCount); + + finalIndexCount = convertedCount; + + (mtlBuffer, offset, type) = _encoderStateManager.IndexBuffer.GetConvertedIndexBuffer(_renderer, Cbs, firstIndex, indexCount, convertedCount, pattern); + } + else + { + (mtlBuffer, offset, type) = _encoderStateManager.IndexBuffer.GetIndexBuffer(_renderer, Cbs); + } + + if (mtlBuffer.NativePtr != IntPtr.Zero) + { + var renderCommandEncoder = GetOrCreateRenderEncoder(true); + + renderCommandEncoder.DrawIndexedPrimitives( + primitiveType, + (ulong)finalIndexCount, + type, + mtlBuffer, + (ulong)offset, + (ulong)instanceCount, + firstVertex, + (ulong)firstInstance); + } + } + + public void DrawIndexedIndirect(BufferRange indirectBuffer) + { + DrawIndexedIndirectOffset(indirectBuffer); + } + + public void DrawIndexedIndirectOffset(BufferRange indirectBuffer, int offset = 0) + { + // TODO: Reindex unsupported topologies + if (TopologyUnsupported(_encoderStateManager.Topology)) + { + Logger.Warning?.Print(LogClass.Gpu, $"Drawing indexed with unsupported topology: {_encoderStateManager.Topology}"); + } + + var buffer = _renderer.BufferManager + .GetBuffer(indirectBuffer.Handle, indirectBuffer.Offset, indirectBuffer.Size, false) + .Get(Cbs, indirectBuffer.Offset, indirectBuffer.Size).Value; + + var primitiveType = TopologyRemap(_encoderStateManager.Topology).Convert(); + + (MTLBuffer indexBuffer, int indexOffset, MTLIndexType type) = _encoderStateManager.IndexBuffer.GetIndexBuffer(_renderer, Cbs); + + if (indexBuffer.NativePtr != IntPtr.Zero && buffer.NativePtr != IntPtr.Zero) + { + var renderCommandEncoder = GetOrCreateRenderEncoder(true); + + renderCommandEncoder.DrawIndexedPrimitives( + primitiveType, + type, + indexBuffer, + (ulong)indexOffset, + buffer, + (ulong)(indirectBuffer.Offset + offset)); + } + } + + public void DrawIndexedIndirectCount(BufferRange indirectBuffer, BufferRange parameterBuffer, int maxDrawCount, int stride) + { + for (int i = 0; i < maxDrawCount; i++) + { + DrawIndexedIndirectOffset(indirectBuffer, stride * i); + } + } + + public void DrawIndirect(BufferRange indirectBuffer) + { + DrawIndirectOffset(indirectBuffer); + } + + public void DrawIndirectOffset(BufferRange indirectBuffer, int offset = 0) + { + if (TopologyUnsupported(_encoderStateManager.Topology)) + { + // TODO: Reindex unsupported topologies + Logger.Warning?.Print(LogClass.Gpu, $"Drawing indirect with unsupported topology: {_encoderStateManager.Topology}"); + } + + var buffer = _renderer.BufferManager + .GetBuffer(indirectBuffer.Handle, indirectBuffer.Offset, indirectBuffer.Size, false) + .Get(Cbs, indirectBuffer.Offset, indirectBuffer.Size).Value; + + var primitiveType = TopologyRemap(_encoderStateManager.Topology).Convert(); + var renderCommandEncoder = GetOrCreateRenderEncoder(true); + + renderCommandEncoder.DrawPrimitives( + primitiveType, + buffer, + (ulong)(indirectBuffer.Offset + offset)); + } + + public void DrawIndirectCount(BufferRange indirectBuffer, BufferRange parameterBuffer, int maxDrawCount, int stride) + { + for (int i = 0; i < maxDrawCount; i++) + { + DrawIndirectOffset(indirectBuffer, stride * i); + } + } + + public void DrawTexture(ITexture texture, ISampler sampler, Extents2DF srcRegion, Extents2DF dstRegion) + { + _renderer.HelperShader.DrawTexture(texture, sampler, srcRegion, dstRegion); + } + + public void SetAlphaTest(bool enable, float reference, CompareOp op) + { + // This is currently handled using shader specialization, as Metal does not support alpha test. + // In the future, we may want to use this to write the reference value into the support buffer, + // to avoid creating one version of the shader per reference value used. + } + + public void SetBlendState(AdvancedBlendDescriptor blend) + { + // Metal does not support advanced blend. + } + + public void SetBlendState(int index, BlendDescriptor blend) + { + _encoderStateManager.UpdateBlendDescriptors(index, blend); + } + + public void SetDepthBias(PolygonModeMask enables, float factor, float units, float clamp) + { + if (enables == 0) + { + _encoderStateManager.UpdateDepthBias(0, 0, 0); + } + else + { + _encoderStateManager.UpdateDepthBias(units, factor, clamp); + } + } + + public void SetDepthClamp(bool clamp) + { + _encoderStateManager.UpdateDepthClamp(clamp); + } + + public void SetDepthMode(DepthMode mode) + { + // Metal does not support depth clip control. + } + + public void SetDepthTest(DepthTestDescriptor depthTest) + { + _encoderStateManager.UpdateDepthState(depthTest); + } + + public void SetFaceCulling(bool enable, Face face) + { + _encoderStateManager.UpdateCullMode(enable, face); + } + + public void SetFrontFace(FrontFace frontFace) + { + _encoderStateManager.UpdateFrontFace(frontFace); + } + + public void SetIndexBuffer(BufferRange buffer, IndexType type) + { + _encoderStateManager.UpdateIndexBuffer(buffer, type); + } + + public void SetImage(ShaderStage stage, int binding, ITexture image) + { + if (image is TextureBase img) + { + _encoderStateManager.UpdateImage(stage, binding, img); + } + } + + public void SetImageArray(ShaderStage stage, int binding, IImageArray array) + { + if (array is ImageArray imageArray) + { + _encoderStateManager.UpdateImageArray(stage, binding, imageArray); + } + } + + public void SetImageArraySeparate(ShaderStage stage, int setIndex, IImageArray array) + { + if (array is ImageArray imageArray) + { + _encoderStateManager.UpdateImageArraySeparate(stage, setIndex, imageArray); + } + } + + public void SetLineParameters(float width, bool smooth) + { + // Metal does not support wide-lines. + } + + public void SetLogicOpState(bool enable, LogicalOp op) + { + _encoderStateManager.UpdateLogicOpState(enable, op); + } + + public void SetMultisampleState(MultisampleDescriptor multisample) + { + _encoderStateManager.UpdateMultisampleState(multisample); + } + + public void SetPatchParameters(int vertices, ReadOnlySpan defaultOuterLevel, ReadOnlySpan defaultInnerLevel) + { + Logger.Warning?.Print(LogClass.Gpu, "Not Implemented!"); + } + + public void SetPointParameters(float size, bool isProgramPointSize, bool enablePointSprite, Origin origin) + { + Logger.Warning?.Print(LogClass.Gpu, "Not Implemented!"); + } + + public void SetPolygonMode(PolygonMode frontMode, PolygonMode backMode) + { + // Metal does not support polygon mode. + } + + public void SetPrimitiveRestart(bool enable, int index) + { + // Always active for LineStrip and TriangleStrip + // https://github.com/gpuweb/gpuweb/issues/1220#issuecomment-732483263 + // https://developer.apple.com/documentation/metal/mtlrendercommandencoder/1515520-drawindexedprimitives + // https://stackoverflow.com/questions/70813665/how-to-render-multiple-trianglestrips-using-metal + + // Emulating disabling this is very difficult. It's unlikely for an index buffer to use the largest possible index, + // so it's fine nearly all of the time. + } + + public void SetPrimitiveTopology(PrimitiveTopology topology) + { + _encoderStateManager.UpdatePrimitiveTopology(topology); + } + + public void SetProgram(IProgram program) + { + _encoderStateManager.UpdateProgram(program); + } + + public void SetRasterizerDiscard(bool discard) + { + _encoderStateManager.UpdateRasterizerDiscard(discard); + } + + public void SetRenderTargetColorMasks(ReadOnlySpan componentMask) + { + _encoderStateManager.UpdateRenderTargetColorMasks(componentMask); + } + + public void SetRenderTargets(ITexture[] colors, ITexture depthStencil) + { + _encoderStateManager.UpdateRenderTargets(colors, depthStencil); + } + + public void SetScissors(ReadOnlySpan> regions) + { + _encoderStateManager.UpdateScissors(regions); + } + + public void SetStencilTest(StencilTestDescriptor stencilTest) + { + _encoderStateManager.UpdateStencilState(stencilTest); + } + + public void SetUniformBuffers(ReadOnlySpan buffers) + { + _encoderStateManager.UpdateUniformBuffers(buffers); + } + + public void SetStorageBuffers(ReadOnlySpan buffers) + { + _encoderStateManager.UpdateStorageBuffers(buffers); + } + + internal void SetStorageBuffers(int first, ReadOnlySpan> buffers) + { + _encoderStateManager.UpdateStorageBuffers(first, buffers); + } + + public void SetTextureAndSampler(ShaderStage stage, int binding, ITexture texture, ISampler sampler) + { + if (texture is TextureBase tex) + { + if (sampler == null || sampler is SamplerHolder) + { + _encoderStateManager.UpdateTextureAndSampler(stage, binding, tex, (SamplerHolder)sampler); + } + } + } + + public void SetTextureArray(ShaderStage stage, int binding, ITextureArray array) + { + if (array is TextureArray textureArray) + { + _encoderStateManager.UpdateTextureArray(stage, binding, textureArray); + } + } + + public void SetTextureArraySeparate(ShaderStage stage, int setIndex, ITextureArray array) + { + if (array is TextureArray textureArray) + { + _encoderStateManager.UpdateTextureArraySeparate(stage, setIndex, textureArray); + } + } + + public void SetUserClipDistance(int index, bool enableClip) + { + // TODO. Same as Vulkan + } + + public void SetVertexAttribs(ReadOnlySpan vertexAttribs) + { + _encoderStateManager.UpdateVertexAttribs(vertexAttribs); + } + + public void SetVertexBuffers(ReadOnlySpan vertexBuffers) + { + _encoderStateManager.UpdateVertexBuffers(vertexBuffers); + } + + public void SetViewports(ReadOnlySpan viewports) + { + _encoderStateManager.UpdateViewports(viewports); + } + + public void TextureBarrier() + { + if (CurrentEncoderType == EncoderType.Render) + { + Encoders.RenderEncoder.MemoryBarrier(MTLBarrierScope.Textures, MTLRenderStages.RenderStageFragment, MTLRenderStages.RenderStageFragment); + } + } + + public void TextureBarrierTiled() + { + TextureBarrier(); + } + + public bool TryHostConditionalRendering(ICounterEvent value, ulong compare, bool isEqual) + { + // TODO: Implementable via indirect draw commands + return false; + } + + public bool TryHostConditionalRendering(ICounterEvent value, ICounterEvent compare, bool isEqual) + { + // TODO: Implementable via indirect draw commands + return false; + } + + public void EndHostConditionalRendering() + { + // TODO: Implementable via indirect draw commands + } + + public void BeginTransformFeedback(PrimitiveTopology topology) + { + // Metal does not support transform feedback. + } + + public void EndTransformFeedback() + { + // Metal does not support transform feedback. + } + + public void SetTransformFeedbackBuffers(ReadOnlySpan buffers) + { + // Metal does not support transform feedback. + } + + public void Dispose() + { + EndCurrentPass(); + _encoderStateManager.Dispose(); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/Program.cs b/src/Ryujinx.Graphics.Metal/Program.cs new file mode 100644 index 000000000..37bae5817 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Program.cs @@ -0,0 +1,286 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Shader; +using SharpMetal.Foundation; +using SharpMetal.Metal; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + class Program : IProgram + { + private ProgramLinkStatus _status; + private readonly ShaderSource[] _shaders; + private readonly GCHandle[] _handles; + private int _successCount; + + private readonly MetalRenderer _renderer; + + public MTLFunction VertexFunction; + public MTLFunction FragmentFunction; + public MTLFunction ComputeFunction; + public ComputeSize ComputeLocalSize { get; } + + private HashTableSlim _graphicsPipelineCache; + private MTLComputePipelineState? _computePipelineCache; + private bool _firstBackgroundUse; + + public ResourceBindingSegment[][] BindingSegments { get; } + // Argument buffer sizes for Vertex or Compute stages + public int[] ArgumentBufferSizes { get; } + // Argument buffer sizes for Fragment stage + public int[] FragArgumentBufferSizes { get; } + + public Program( + MetalRenderer renderer, + MTLDevice device, + ShaderSource[] shaders, + ResourceLayout resourceLayout, + ComputeSize computeLocalSize = default) + { + _renderer = renderer; + renderer.Programs.Add(this); + + ComputeLocalSize = computeLocalSize; + _shaders = shaders; + _handles = new GCHandle[_shaders.Length]; + + _status = ProgramLinkStatus.Incomplete; + + for (int i = 0; i < _shaders.Length; i++) + { + ShaderSource shader = _shaders[i]; + + using var compileOptions = new MTLCompileOptions + { + PreserveInvariance = true, + LanguageVersion = MTLLanguageVersion.Version31, + }; + var index = i; + + _handles[i] = device.NewLibrary(StringHelper.NSString(shader.Code), compileOptions, (library, error) => CompilationResultHandler(library, error, index)); + } + + (BindingSegments, ArgumentBufferSizes, FragArgumentBufferSizes) = BuildBindingSegments(resourceLayout.SetUsages); + } + + public void CompilationResultHandler(MTLLibrary library, NSError error, int index) + { + var shader = _shaders[index]; + + if (_handles[index].IsAllocated) + { + _handles[index].Free(); + } + + if (error != IntPtr.Zero) + { + Logger.Warning?.PrintMsg(LogClass.Gpu, shader.Code); + Logger.Warning?.Print(LogClass.Gpu, $"{shader.Stage} shader linking failed: \n{StringHelper.String(error.LocalizedDescription)}"); + _status = ProgramLinkStatus.Failure; + return; + } + + switch (shader.Stage) + { + case ShaderStage.Compute: + ComputeFunction = library.NewFunction(StringHelper.NSString("kernelMain")); + break; + case ShaderStage.Vertex: + VertexFunction = library.NewFunction(StringHelper.NSString("vertexMain")); + break; + case ShaderStage.Fragment: + FragmentFunction = library.NewFunction(StringHelper.NSString("fragmentMain")); + break; + default: + Logger.Warning?.Print(LogClass.Gpu, $"Cannot handle stage {shader.Stage}!"); + break; + } + + _successCount++; + + if (_successCount >= _shaders.Length && _status != ProgramLinkStatus.Failure) + { + _status = ProgramLinkStatus.Success; + } + } + + private static (ResourceBindingSegment[][], int[], int[]) BuildBindingSegments(ReadOnlyCollection setUsages) + { + ResourceBindingSegment[][] segments = new ResourceBindingSegment[setUsages.Count][]; + int[] argBufferSizes = new int[setUsages.Count]; + int[] fragArgBufferSizes = new int[setUsages.Count]; + + for (int setIndex = 0; setIndex < setUsages.Count; setIndex++) + { + List currentSegments = new(); + + ResourceUsage currentUsage = default; + int currentCount = 0; + + for (int index = 0; index < setUsages[setIndex].Usages.Count; index++) + { + ResourceUsage usage = setUsages[setIndex].Usages[index]; + + if (currentUsage.Binding + currentCount != usage.Binding || + currentUsage.Type != usage.Type || + currentUsage.Stages != usage.Stages || + currentUsage.ArrayLength > 1 || + usage.ArrayLength > 1) + { + if (currentCount != 0) + { + currentSegments.Add(new ResourceBindingSegment( + currentUsage.Binding, + currentCount, + currentUsage.Type, + currentUsage.Stages, + currentUsage.ArrayLength > 1)); + + var size = currentCount * ResourcePointerSize(currentUsage.Type); + if (currentUsage.Stages.HasFlag(ResourceStages.Fragment)) + { + fragArgBufferSizes[setIndex] += size; + } + + if (currentUsage.Stages.HasFlag(ResourceStages.Vertex) || + currentUsage.Stages.HasFlag(ResourceStages.Compute)) + { + argBufferSizes[setIndex] += size; + } + } + + currentUsage = usage; + currentCount = usage.ArrayLength; + } + else + { + currentCount++; + } + } + + if (currentCount != 0) + { + currentSegments.Add(new ResourceBindingSegment( + currentUsage.Binding, + currentCount, + currentUsage.Type, + currentUsage.Stages, + currentUsage.ArrayLength > 1)); + + var size = currentCount * ResourcePointerSize(currentUsage.Type); + if (currentUsage.Stages.HasFlag(ResourceStages.Fragment)) + { + fragArgBufferSizes[setIndex] += size; + } + + if (currentUsage.Stages.HasFlag(ResourceStages.Vertex) || + currentUsage.Stages.HasFlag(ResourceStages.Compute)) + { + argBufferSizes[setIndex] += size; + } + } + + segments[setIndex] = currentSegments.ToArray(); + } + + return (segments, argBufferSizes, fragArgBufferSizes); + } + + private static int ResourcePointerSize(ResourceType type) + { + return (type == ResourceType.TextureAndSampler ? 2 : 1); + } + + public ProgramLinkStatus CheckProgramLink(bool blocking) + { + if (blocking) + { + while (_status == ProgramLinkStatus.Incomplete) + { } + + return _status; + } + + return _status; + } + + public byte[] GetBinary() + { + return []; + } + + public void AddGraphicsPipeline(ref PipelineUid key, MTLRenderPipelineState pipeline) + { + (_graphicsPipelineCache ??= new()).Add(ref key, pipeline); + } + + public void AddComputePipeline(MTLComputePipelineState pipeline) + { + _computePipelineCache = pipeline; + } + + public bool TryGetGraphicsPipeline(ref PipelineUid key, out MTLRenderPipelineState pipeline) + { + if (_graphicsPipelineCache == null) + { + pipeline = default; + return false; + } + + if (!_graphicsPipelineCache.TryGetValue(ref key, out pipeline)) + { + if (_firstBackgroundUse) + { + Logger.Warning?.Print(LogClass.Gpu, "Background pipeline compile missed on draw - incorrect pipeline state?"); + _firstBackgroundUse = false; + } + + return false; + } + + _firstBackgroundUse = false; + + return true; + } + + public bool TryGetComputePipeline(out MTLComputePipelineState pipeline) + { + if (_computePipelineCache.HasValue) + { + pipeline = _computePipelineCache.Value; + return true; + } + + pipeline = default; + return false; + } + + public void Dispose() + { + if (!_renderer.Programs.Remove(this)) + { + return; + } + + if (_graphicsPipelineCache != null) + { + foreach (MTLRenderPipelineState pipeline in _graphicsPipelineCache.Values) + { + pipeline.Dispose(); + } + } + + _computePipelineCache?.Dispose(); + + VertexFunction.Dispose(); + FragmentFunction.Dispose(); + ComputeFunction.Dispose(); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/ResourceBindingSegment.cs b/src/Ryujinx.Graphics.Metal/ResourceBindingSegment.cs new file mode 100644 index 000000000..8e6d88c4b --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/ResourceBindingSegment.cs @@ -0,0 +1,22 @@ +using Ryujinx.Graphics.GAL; + +namespace Ryujinx.Graphics.Metal +{ + readonly struct ResourceBindingSegment + { + public readonly int Binding; + public readonly int Count; + public readonly ResourceType Type; + public readonly ResourceStages Stages; + public readonly bool IsArray; + + public ResourceBindingSegment(int binding, int count, ResourceType type, ResourceStages stages, bool isArray) + { + Binding = binding; + Count = count; + Type = type; + Stages = stages; + IsArray = isArray; + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/ResourceLayoutBuilder.cs b/src/Ryujinx.Graphics.Metal/ResourceLayoutBuilder.cs new file mode 100644 index 000000000..36ae9bac6 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/ResourceLayoutBuilder.cs @@ -0,0 +1,59 @@ +using Ryujinx.Graphics.GAL; +using System; +using System.Collections.Generic; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + class ResourceLayoutBuilder + { + private const int TotalSets = MetalRenderer.TotalSets; + + private readonly List[] _resourceDescriptors; + private readonly List[] _resourceUsages; + + public ResourceLayoutBuilder() + { + _resourceDescriptors = new List[TotalSets]; + _resourceUsages = new List[TotalSets]; + + for (int index = 0; index < TotalSets; index++) + { + _resourceDescriptors[index] = new(); + _resourceUsages[index] = new(); + } + } + + public ResourceLayoutBuilder Add(ResourceStages stages, ResourceType type, int binding, bool write = false) + { + uint setIndex = type switch + { + ResourceType.UniformBuffer => Constants.ConstantBuffersSetIndex, + ResourceType.StorageBuffer => Constants.StorageBuffersSetIndex, + ResourceType.TextureAndSampler or ResourceType.BufferTexture => Constants.TexturesSetIndex, + ResourceType.Image or ResourceType.BufferImage => Constants.ImagesSetIndex, + _ => throw new ArgumentException($"Invalid resource type \"{type}\"."), + }; + + _resourceDescriptors[setIndex].Add(new ResourceDescriptor(binding, 1, type, stages)); + _resourceUsages[setIndex].Add(new ResourceUsage(binding, 1, type, stages, write)); + + return this; + } + + public ResourceLayout Build() + { + var descriptors = new ResourceDescriptorCollection[TotalSets]; + var usages = new ResourceUsageCollection[TotalSets]; + + for (int index = 0; index < TotalSets; index++) + { + descriptors[index] = new ResourceDescriptorCollection(_resourceDescriptors[index].ToArray().AsReadOnly()); + usages[index] = new ResourceUsageCollection(_resourceUsages[index].ToArray().AsReadOnly()); + } + + return new ResourceLayout(descriptors.AsReadOnly(), usages.AsReadOnly()); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/Ryujinx.Graphics.Metal.csproj b/src/Ryujinx.Graphics.Metal/Ryujinx.Graphics.Metal.csproj new file mode 100644 index 000000000..02afb150a --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Ryujinx.Graphics.Metal.csproj @@ -0,0 +1,30 @@ + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ryujinx.Graphics.Metal/SamplerHolder.cs b/src/Ryujinx.Graphics.Metal/SamplerHolder.cs new file mode 100644 index 000000000..3241efa6d --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/SamplerHolder.cs @@ -0,0 +1,90 @@ +using Ryujinx.Graphics.GAL; +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + class SamplerHolder : ISampler + { + private readonly MetalRenderer _renderer; + private readonly Auto _sampler; + + public SamplerHolder(MetalRenderer renderer, MTLDevice device, SamplerCreateInfo info) + { + _renderer = renderer; + + renderer.Samplers.Add(this); + + (MTLSamplerMinMagFilter minFilter, MTLSamplerMipFilter mipFilter) = info.MinFilter.Convert(); + + MTLSamplerBorderColor borderColor = GetConstrainedBorderColor(info.BorderColor, out _); + + using var descriptor = new MTLSamplerDescriptor + { + BorderColor = borderColor, + MinFilter = minFilter, + MagFilter = info.MagFilter.Convert(), + MipFilter = mipFilter, + CompareFunction = info.CompareOp.Convert(), + LodMinClamp = info.MinLod, + LodMaxClamp = info.MaxLod, + LodAverage = false, + MaxAnisotropy = Math.Max((uint)info.MaxAnisotropy, 1), + SAddressMode = info.AddressU.Convert(), + TAddressMode = info.AddressV.Convert(), + RAddressMode = info.AddressP.Convert(), + SupportArgumentBuffers = true + }; + + var sampler = device.NewSamplerState(descriptor); + + _sampler = new Auto(new DisposableSampler(sampler)); + } + + private static MTLSamplerBorderColor GetConstrainedBorderColor(ColorF arbitraryBorderColor, out bool cantConstrain) + { + float r = arbitraryBorderColor.Red; + float g = arbitraryBorderColor.Green; + float b = arbitraryBorderColor.Blue; + float a = arbitraryBorderColor.Alpha; + + if (r == 0f && g == 0f && b == 0f) + { + if (a == 1f) + { + cantConstrain = false; + return MTLSamplerBorderColor.OpaqueBlack; + } + + if (a == 0f) + { + cantConstrain = false; + return MTLSamplerBorderColor.TransparentBlack; + } + } + else if (r == 1f && g == 1f && b == 1f && a == 1f) + { + cantConstrain = false; + return MTLSamplerBorderColor.OpaqueWhite; + } + + cantConstrain = true; + return MTLSamplerBorderColor.OpaqueBlack; + } + + public Auto GetSampler() + { + return _sampler; + } + + public void Dispose() + { + if (_renderer.Samplers.Remove(this)) + { + _sampler.Dispose(); + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/Shaders/Blit.metal b/src/Ryujinx.Graphics.Metal/Shaders/Blit.metal new file mode 100644 index 000000000..887878499 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Shaders/Blit.metal @@ -0,0 +1,43 @@ +#include + +using namespace metal; + +struct CopyVertexOut { + float4 position [[position]]; + float2 uv; +}; + +struct TexCoords { + float data[4]; +}; + +struct ConstantBuffers { + constant TexCoords* tex_coord; +}; + +struct Textures +{ + texture2d texture; + sampler sampler; +}; + +vertex CopyVertexOut vertexMain(uint vid [[vertex_id]], + constant ConstantBuffers &constant_buffers [[buffer(CONSTANT_BUFFERS_INDEX)]]) { + CopyVertexOut out; + + int low = vid & 1; + int high = vid >> 1; + out.uv.x = constant_buffers.tex_coord->data[low]; + out.uv.y = constant_buffers.tex_coord->data[2 + high]; + out.position.x = (float(low) - 0.5f) * 2.0f; + out.position.y = (float(high) - 0.5f) * 2.0f; + out.position.z = 0.0f; + out.position.w = 1.0f; + + return out; +} + +fragment FORMAT4 fragmentMain(CopyVertexOut in [[stage_in]], + constant Textures &textures [[buffer(TEXTURES_INDEX)]]) { + return textures.texture.sample(textures.sampler, in.uv); +} diff --git a/src/Ryujinx.Graphics.Metal/Shaders/BlitMs.metal b/src/Ryujinx.Graphics.Metal/Shaders/BlitMs.metal new file mode 100644 index 000000000..1077b6cea --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Shaders/BlitMs.metal @@ -0,0 +1,45 @@ +#include + +using namespace metal; + +struct CopyVertexOut { + float4 position [[position]]; + float2 uv; +}; + +struct TexCoords { + float data[4]; +}; + +struct ConstantBuffers { + constant TexCoords* tex_coord; +}; + +struct Textures +{ + texture2d_ms texture; +}; + +vertex CopyVertexOut vertexMain(uint vid [[vertex_id]], + constant ConstantBuffers &constant_buffers [[buffer(CONSTANT_BUFFERS_INDEX)]]) { + CopyVertexOut out; + + int low = vid & 1; + int high = vid >> 1; + out.uv.x = constant_buffers.tex_coord->data[low]; + out.uv.y = constant_buffers.tex_coord->data[2 + high]; + out.position.x = (float(low) - 0.5f) * 2.0f; + out.position.y = (float(high) - 0.5f) * 2.0f; + out.position.z = 0.0f; + out.position.w = 1.0f; + + return out; +} + +fragment FORMAT4 fragmentMain(CopyVertexOut in [[stage_in]], + constant Textures &textures [[buffer(TEXTURES_INDEX)]], + uint sample_id [[sample_id]]) { + uint2 tex_size = uint2(textures.texture.get_width(), textures.texture.get_height()); + uint2 tex_coord = uint2(in.uv * float2(tex_size)); + return textures.texture.read(tex_coord, sample_id); +} diff --git a/src/Ryujinx.Graphics.Metal/Shaders/ChangeBufferStride.metal b/src/Ryujinx.Graphics.Metal/Shaders/ChangeBufferStride.metal new file mode 100644 index 000000000..1a7d2c574 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Shaders/ChangeBufferStride.metal @@ -0,0 +1,72 @@ +#include + +using namespace metal; + +struct StrideArguments { + int4 data; +}; + +struct InData { + uint8_t data[1]; +}; + +struct OutData { + uint8_t data[1]; +}; + +struct ConstantBuffers { + constant StrideArguments* stride_arguments; +}; + +struct StorageBuffers { + device InData* in_data; + device OutData* out_data; +}; + +kernel void kernelMain(constant ConstantBuffers &constant_buffers [[buffer(CONSTANT_BUFFERS_INDEX)]], + device StorageBuffers &storage_buffers [[buffer(STORAGE_BUFFERS_INDEX)]], + uint3 thread_position_in_grid [[thread_position_in_grid]], + uint3 threads_per_threadgroup [[threads_per_threadgroup]], + uint3 threadgroups_per_grid [[threadgroups_per_grid]]) +{ + // Determine what slice of the stride copies this invocation will perform. + + int sourceStride = constant_buffers.stride_arguments->data.x; + int targetStride = constant_buffers.stride_arguments->data.y; + int bufferSize = constant_buffers.stride_arguments->data.z; + int sourceOffset = constant_buffers.stride_arguments->data.w; + + int strideRemainder = targetStride - sourceStride; + int invocations = int(threads_per_threadgroup.x * threadgroups_per_grid.x); + + int copiesRequired = bufferSize / sourceStride; + + // Find the copies that this invocation should perform. + + // - Copies that all invocations perform. + int allInvocationCopies = copiesRequired / invocations; + + // - Extra remainder copy that this invocation performs. + int index = int(thread_position_in_grid.x); + int extra = (index < (copiesRequired % invocations)) ? 1 : 0; + + int copyCount = allInvocationCopies + extra; + + // Finally, get the starting offset. Make sure to count extra copies. + + int startCopy = allInvocationCopies * index + min(copiesRequired % invocations, index); + + int srcOffset = sourceOffset + startCopy * sourceStride; + int dstOffset = startCopy * targetStride; + + // Perform the copies for this region + for (int i = 0; i < copyCount; i++) { + for (int j = 0; j < sourceStride; j++) { + storage_buffers.out_data->data[dstOffset++] = storage_buffers.in_data->data[srcOffset++]; + } + + for (int j = 0; j < strideRemainder; j++) { + storage_buffers.out_data->data[dstOffset++] = uint8_t(0); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/Shaders/ColorClear.metal b/src/Ryujinx.Graphics.Metal/Shaders/ColorClear.metal new file mode 100644 index 000000000..46a57e035 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Shaders/ColorClear.metal @@ -0,0 +1,38 @@ +#include + +using namespace metal; + +struct VertexOut { + float4 position [[position]]; +}; + +struct ClearColor { + FORMAT4 data; +}; + +struct ConstantBuffers { + constant ClearColor* clear_color; +}; + +vertex VertexOut vertexMain(ushort vid [[vertex_id]]) { + int low = vid & 1; + int high = vid >> 1; + + VertexOut out; + + out.position.x = (float(low) - 0.5f) * 2.0f; + out.position.y = (float(high) - 0.5f) * 2.0f; + out.position.z = 0.0f; + out.position.w = 1.0f; + + return out; +} + +struct FragmentOut { + FORMAT4 color [[color(COLOR_ATTACHMENT_INDEX)]]; +}; + +fragment FragmentOut fragmentMain(VertexOut in [[stage_in]], + constant ConstantBuffers &constant_buffers [[buffer(CONSTANT_BUFFERS_INDEX)]]) { + return {constant_buffers.clear_color->data}; +} diff --git a/src/Ryujinx.Graphics.Metal/Shaders/ConvertD32S8ToD24S8.metal b/src/Ryujinx.Graphics.Metal/Shaders/ConvertD32S8ToD24S8.metal new file mode 100644 index 000000000..870ac3d78 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Shaders/ConvertD32S8ToD24S8.metal @@ -0,0 +1,66 @@ +#include + +using namespace metal; + +struct StrideArguments { + int pixelCount; + int dstStartOffset; +}; + +struct InData { + uint data[1]; +}; + +struct OutData { + uint data[1]; +}; + +struct ConstantBuffers { + constant StrideArguments* stride_arguments; +}; + +struct StorageBuffers { + device InData* in_data; + device OutData* out_data; +}; + +kernel void kernelMain(constant ConstantBuffers &constant_buffers [[buffer(CONSTANT_BUFFERS_INDEX)]], + device StorageBuffers &storage_buffers [[buffer(STORAGE_BUFFERS_INDEX)]], + uint3 thread_position_in_grid [[thread_position_in_grid]], + uint3 threads_per_threadgroup [[threads_per_threadgroup]], + uint3 threadgroups_per_grid [[threadgroups_per_grid]]) +{ + // Determine what slice of the stride copies this invocation will perform. + int invocations = int(threads_per_threadgroup.x * threadgroups_per_grid.x); + + int copiesRequired = constant_buffers.stride_arguments->pixelCount; + + // Find the copies that this invocation should perform. + + // - Copies that all invocations perform. + int allInvocationCopies = copiesRequired / invocations; + + // - Extra remainder copy that this invocation performs. + int index = int(thread_position_in_grid.x); + int extra = (index < (copiesRequired % invocations)) ? 1 : 0; + + int copyCount = allInvocationCopies + extra; + + // Finally, get the starting offset. Make sure to count extra copies. + + int startCopy = allInvocationCopies * index + min(copiesRequired % invocations, index); + + int srcOffset = startCopy * 2; + int dstOffset = constant_buffers.stride_arguments->dstStartOffset + startCopy; + + // Perform the conversion for this region. + for (int i = 0; i < copyCount; i++) + { + float depth = as_type(storage_buffers.in_data->data[srcOffset++]); + uint stencil = storage_buffers.in_data->data[srcOffset++]; + + uint rescaledDepth = uint(clamp(depth, 0.0, 1.0) * 16777215.0); + + storage_buffers.out_data->data[dstOffset++] = (rescaledDepth << 8) | (stencil & 0xff); + } +} diff --git a/src/Ryujinx.Graphics.Metal/Shaders/ConvertIndexBuffer.metal b/src/Ryujinx.Graphics.Metal/Shaders/ConvertIndexBuffer.metal new file mode 100644 index 000000000..c8fee5818 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Shaders/ConvertIndexBuffer.metal @@ -0,0 +1,59 @@ +#include + +using namespace metal; + +struct IndexBufferPattern { + int pattern[8]; + int primitiveVertices; + int primitiveVerticesOut; + int indexSize; + int indexSizeOut; + int baseIndex; + int indexStride; + int srcOffset; + int totalPrimitives; +}; + +struct InData { + uint8_t data[1]; +}; + +struct OutData { + uint8_t data[1]; +}; + +struct StorageBuffers { + device InData* in_data; + device OutData* out_data; + constant IndexBufferPattern* index_buffer_pattern; +}; + +kernel void kernelMain(device StorageBuffers &storage_buffers [[buffer(STORAGE_BUFFERS_INDEX)]], + uint3 thread_position_in_grid [[thread_position_in_grid]]) +{ + int primitiveIndex = int(thread_position_in_grid.x); + if (primitiveIndex >= storage_buffers.index_buffer_pattern->totalPrimitives) + { + return; + } + + int inOffset = primitiveIndex * storage_buffers.index_buffer_pattern->indexStride; + int outOffset = primitiveIndex * storage_buffers.index_buffer_pattern->primitiveVerticesOut; + + for (int i = 0; i < storage_buffers.index_buffer_pattern->primitiveVerticesOut; i++) + { + int j; + int io = max(0, inOffset + storage_buffers.index_buffer_pattern->baseIndex + storage_buffers.index_buffer_pattern->pattern[i]) * storage_buffers.index_buffer_pattern->indexSize; + int oo = (outOffset + i) * storage_buffers.index_buffer_pattern->indexSizeOut; + + for (j = 0; j < storage_buffers.index_buffer_pattern->indexSize; j++) + { + storage_buffers.out_data->data[oo + j] = storage_buffers.in_data->data[storage_buffers.index_buffer_pattern->srcOffset + io + j]; + } + + for(; j < storage_buffers.index_buffer_pattern->indexSizeOut; j++) + { + storage_buffers.out_data->data[oo + j] = uint8_t(0); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/Shaders/DepthBlit.metal b/src/Ryujinx.Graphics.Metal/Shaders/DepthBlit.metal new file mode 100644 index 000000000..8b8467c2f --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Shaders/DepthBlit.metal @@ -0,0 +1,27 @@ +#include + +using namespace metal; + +struct CopyVertexOut { + float4 position [[position]]; + float2 uv; +}; + +struct Textures +{ + texture2d texture; + sampler sampler; +}; + +struct FragmentOut { + float depth [[depth(any)]]; +}; + +fragment FragmentOut fragmentMain(CopyVertexOut in [[stage_in]], + constant Textures &textures [[buffer(TEXTURES_INDEX)]]) { + FragmentOut out; + + out.depth = textures.texture.sample(textures.sampler, in.uv).r; + + return out; +} diff --git a/src/Ryujinx.Graphics.Metal/Shaders/DepthBlitMs.metal b/src/Ryujinx.Graphics.Metal/Shaders/DepthBlitMs.metal new file mode 100644 index 000000000..10791f636 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Shaders/DepthBlitMs.metal @@ -0,0 +1,29 @@ +#include + +using namespace metal; + +struct CopyVertexOut { + float4 position [[position]]; + float2 uv; +}; + +struct Textures +{ + texture2d_ms texture; +}; + +struct FragmentOut { + float depth [[depth(any)]]; +}; + +fragment FragmentOut fragmentMain(CopyVertexOut in [[stage_in]], + constant Textures &textures [[buffer(TEXTURES_INDEX)]], + uint sample_id [[sample_id]]) { + FragmentOut out; + + uint2 tex_size = uint2(textures.texture.get_width(), textures.texture.get_height()); + uint2 tex_coord = uint2(in.uv * float2(tex_size)); + out.depth = textures.texture.read(tex_coord, sample_id).r; + + return out; +} diff --git a/src/Ryujinx.Graphics.Metal/Shaders/DepthStencilClear.metal b/src/Ryujinx.Graphics.Metal/Shaders/DepthStencilClear.metal new file mode 100644 index 000000000..7e50f2ce7 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Shaders/DepthStencilClear.metal @@ -0,0 +1,42 @@ +#include + +using namespace metal; + +struct VertexOut { + float4 position [[position]]; +}; + +struct FragmentOut { + float depth [[depth(any)]]; +}; + +struct ClearDepth { + float data; +}; + +struct ConstantBuffers { + constant ClearDepth* clear_depth; +}; + +vertex VertexOut vertexMain(ushort vid [[vertex_id]]) { + int low = vid & 1; + int high = vid >> 1; + + VertexOut out; + + out.position.x = (float(low) - 0.5f) * 2.0f; + out.position.y = (float(high) - 0.5f) * 2.0f; + out.position.z = 0.0f; + out.position.w = 1.0f; + + return out; +} + +fragment FragmentOut fragmentMain(VertexOut in [[stage_in]], + constant ConstantBuffers &constant_buffers [[buffer(CONSTANT_BUFFERS_INDEX)]]) { + FragmentOut out; + + out.depth = constant_buffers.clear_depth->data; + + return out; +} diff --git a/src/Ryujinx.Graphics.Metal/Shaders/StencilBlit.metal b/src/Ryujinx.Graphics.Metal/Shaders/StencilBlit.metal new file mode 100644 index 000000000..0b25f322d --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Shaders/StencilBlit.metal @@ -0,0 +1,27 @@ +#include + +using namespace metal; + +struct CopyVertexOut { + float4 position [[position]]; + float2 uv; +}; + +struct Textures +{ + texture2d texture; + sampler sampler; +}; + +struct FragmentOut { + uint stencil [[stencil]]; +}; + +fragment FragmentOut fragmentMain(CopyVertexOut in [[stage_in]], + constant Textures &textures [[buffer(TEXTURES_INDEX)]]) { + FragmentOut out; + + out.stencil = textures.texture.sample(textures.sampler, in.uv).r; + + return out; +} diff --git a/src/Ryujinx.Graphics.Metal/Shaders/StencilBlitMs.metal b/src/Ryujinx.Graphics.Metal/Shaders/StencilBlitMs.metal new file mode 100644 index 000000000..e7f2d20b7 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Shaders/StencilBlitMs.metal @@ -0,0 +1,29 @@ +#include + +using namespace metal; + +struct CopyVertexOut { + float4 position [[position]]; + float2 uv; +}; + +struct Textures +{ + texture2d_ms texture; +}; + +struct FragmentOut { + uint stencil [[stencil]]; +}; + +fragment FragmentOut fragmentMain(CopyVertexOut in [[stage_in]], + constant Textures &textures [[buffer(TEXTURES_INDEX)]], + uint sample_id [[sample_id]]) { + FragmentOut out; + + uint2 tex_size = uint2(textures.texture.get_width(), textures.texture.get_height()); + uint2 tex_coord = uint2(in.uv * float2(tex_size)); + out.stencil = textures.texture.read(tex_coord, sample_id).r; + + return out; +} diff --git a/src/Ryujinx.Graphics.Metal/StagingBuffer.cs b/src/Ryujinx.Graphics.Metal/StagingBuffer.cs new file mode 100644 index 000000000..b250b87f2 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/StagingBuffer.cs @@ -0,0 +1,288 @@ +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.GAL; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + readonly struct StagingBufferReserved + { + public readonly BufferHolder Buffer; + public readonly int Offset; + public readonly int Size; + + public StagingBufferReserved(BufferHolder buffer, int offset, int size) + { + Buffer = buffer; + Offset = offset; + Size = size; + } + } + + [SupportedOSPlatform("macos")] + class StagingBuffer : IDisposable + { + private const int BufferSize = 32 * 1024 * 1024; + + private int _freeOffset; + private int _freeSize; + + private readonly MetalRenderer _renderer; + private readonly BufferHolder _buffer; + private readonly int _resourceAlignment; + + public readonly BufferHandle Handle; + + private readonly struct PendingCopy + { + public FenceHolder Fence { get; } + public int Size { get; } + + public PendingCopy(FenceHolder fence, int size) + { + Fence = fence; + Size = size; + fence.Get(); + } + } + + private readonly Queue _pendingCopies; + + public StagingBuffer(MetalRenderer renderer, BufferManager bufferManager) + { + _renderer = renderer; + + Handle = bufferManager.CreateWithHandle(BufferSize, out _buffer); + _pendingCopies = new Queue(); + _freeSize = BufferSize; + _resourceAlignment = Constants.MinResourceAlignment; + } + + public void PushData(CommandBufferPool cbp, CommandBufferScoped? cbs, BufferHolder dst, int dstOffset, ReadOnlySpan data) + { + bool isRender = cbs != null; + CommandBufferScoped scoped = cbs ?? cbp.Rent(); + + // Must push all data to the buffer. If it can't fit, split it up. + + while (data.Length > 0) + { + if (_freeSize < data.Length) + { + FreeCompleted(); + } + + while (_freeSize == 0) + { + if (!WaitFreeCompleted(cbp)) + { + if (isRender) + { + _renderer.FlushAllCommands(); + scoped = cbp.Rent(); + isRender = false; + } + else + { + scoped = cbp.ReturnAndRent(scoped); + } + } + } + + int chunkSize = Math.Min(_freeSize, data.Length); + + PushDataImpl(scoped, dst, dstOffset, data[..chunkSize]); + + dstOffset += chunkSize; + data = data[chunkSize..]; + } + + if (!isRender) + { + scoped.Dispose(); + } + } + + private void PushDataImpl(CommandBufferScoped cbs, BufferHolder dst, int dstOffset, ReadOnlySpan data) + { + var srcBuffer = _buffer.GetBuffer(); + var dstBuffer = dst.GetBuffer(dstOffset, data.Length, true); + + int offset = _freeOffset; + int capacity = BufferSize - offset; + if (capacity < data.Length) + { + _buffer.SetDataUnchecked(offset, data[..capacity]); + _buffer.SetDataUnchecked(0, data[capacity..]); + + BufferHolder.Copy(cbs, srcBuffer, dstBuffer, offset, dstOffset, capacity); + BufferHolder.Copy(cbs, srcBuffer, dstBuffer, 0, dstOffset + capacity, data.Length - capacity); + } + else + { + _buffer.SetDataUnchecked(offset, data); + + BufferHolder.Copy(cbs, srcBuffer, dstBuffer, offset, dstOffset, data.Length); + } + + _freeOffset = (offset + data.Length) & (BufferSize - 1); + _freeSize -= data.Length; + Debug.Assert(_freeSize >= 0); + + _pendingCopies.Enqueue(new PendingCopy(cbs.GetFence(), data.Length)); + } + + public bool TryPushData(CommandBufferScoped cbs, BufferHolder dst, int dstOffset, ReadOnlySpan data) + { + if (data.Length > BufferSize) + { + return false; + } + + if (_freeSize < data.Length) + { + FreeCompleted(); + + if (_freeSize < data.Length) + { + return false; + } + } + + PushDataImpl(cbs, dst, dstOffset, data); + + return true; + } + + private StagingBufferReserved ReserveDataImpl(CommandBufferScoped cbs, int size, int alignment) + { + // Assumes the caller has already determined that there is enough space. + int offset = BitUtils.AlignUp(_freeOffset, alignment); + int padding = offset - _freeOffset; + + int capacity = Math.Min(_freeSize, BufferSize - offset); + int reservedLength = size + padding; + if (capacity < size) + { + offset = 0; // Place at start. + reservedLength += capacity; + } + + _freeOffset = (_freeOffset + reservedLength) & (BufferSize - 1); + _freeSize -= reservedLength; + Debug.Assert(_freeSize >= 0); + + _pendingCopies.Enqueue(new PendingCopy(cbs.GetFence(), reservedLength)); + + return new StagingBufferReserved(_buffer, offset, size); + } + + private int GetContiguousFreeSize(int alignment) + { + int alignedFreeOffset = BitUtils.AlignUp(_freeOffset, alignment); + int padding = alignedFreeOffset - _freeOffset; + + // Free regions: + // - Aligned free offset to end (minimum free size - padding) + // - 0 to _freeOffset + freeSize wrapped (only if free area contains 0) + + int endOffset = (_freeOffset + _freeSize) & (BufferSize - 1); + + return Math.Max( + Math.Min(_freeSize - padding, BufferSize - alignedFreeOffset), + endOffset <= _freeOffset ? Math.Min(_freeSize, endOffset) : 0 + ); + } + + /// + /// Reserve a range on the staging buffer for the current command buffer and upload data to it. + /// + /// Command buffer to reserve the data on + /// The minimum size the reserved data requires + /// The required alignment for the buffer offset + /// The reserved range of the staging buffer + public StagingBufferReserved? TryReserveData(CommandBufferScoped cbs, int size, int alignment) + { + if (size > BufferSize) + { + return null; + } + + // Temporary reserved data cannot be fragmented. + + if (GetContiguousFreeSize(alignment) < size) + { + FreeCompleted(); + + if (GetContiguousFreeSize(alignment) < size) + { + Logger.Debug?.PrintMsg(LogClass.Gpu, $"Staging buffer out of space to reserve data of size {size}."); + return null; + } + } + + return ReserveDataImpl(cbs, size, alignment); + } + + /// + /// Reserve a range on the staging buffer for the current command buffer and upload data to it. + /// Uses the most permissive byte alignment. + /// + /// Command buffer to reserve the data on + /// The minimum size the reserved data requires + /// The reserved range of the staging buffer + public StagingBufferReserved? TryReserveData(CommandBufferScoped cbs, int size) + { + return TryReserveData(cbs, size, _resourceAlignment); + } + + private bool WaitFreeCompleted(CommandBufferPool cbp) + { + if (_pendingCopies.TryPeek(out var pc)) + { + if (!pc.Fence.IsSignaled()) + { + if (cbp.IsFenceOnRentedCommandBuffer(pc.Fence)) + { + return false; + } + + pc.Fence.Wait(); + } + + var dequeued = _pendingCopies.Dequeue(); + Debug.Assert(dequeued.Fence == pc.Fence); + _freeSize += pc.Size; + pc.Fence.Put(); + } + + return true; + } + + public void FreeCompleted() + { + FenceHolder signalledFence = null; + while (_pendingCopies.TryPeek(out var pc) && (pc.Fence == signalledFence || pc.Fence.IsSignaled())) + { + signalledFence = pc.Fence; // Already checked - don't need to do it again. + var dequeued = _pendingCopies.Dequeue(); + Debug.Assert(dequeued.Fence == pc.Fence); + _freeSize += pc.Size; + pc.Fence.Put(); + } + } + + public void Dispose() + { + _renderer.BufferManager.Delete(Handle); + + while (_pendingCopies.TryDequeue(out var pc)) + { + pc.Fence.Put(); + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/State/DepthStencilUid.cs b/src/Ryujinx.Graphics.Metal/State/DepthStencilUid.cs new file mode 100644 index 000000000..63b1d8ef4 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/State/DepthStencilUid.cs @@ -0,0 +1,110 @@ +using SharpMetal.Metal; +using System; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace Ryujinx.Graphics.Metal.State +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct StencilUid + { + public uint ReadMask; + public uint WriteMask; + public ushort Operations; + + public MTLStencilOperation StencilFailureOperation + { + readonly get => (MTLStencilOperation)((Operations >> 0) & 0xF); + set => Operations = (ushort)((Operations & 0xFFF0) | ((int)value << 0)); + } + + public MTLStencilOperation DepthFailureOperation + { + readonly get => (MTLStencilOperation)((Operations >> 4) & 0xF); + set => Operations = (ushort)((Operations & 0xFF0F) | ((int)value << 4)); + } + + public MTLStencilOperation DepthStencilPassOperation + { + readonly get => (MTLStencilOperation)((Operations >> 8) & 0xF); + set => Operations = (ushort)((Operations & 0xF0FF) | ((int)value << 8)); + } + + public MTLCompareFunction StencilCompareFunction + { + readonly get => (MTLCompareFunction)((Operations >> 12) & 0xF); + set => Operations = (ushort)((Operations & 0x0FFF) | ((int)value << 12)); + } + } + + + [StructLayout(LayoutKind.Explicit, Size = 24)] + internal struct DepthStencilUid : IEquatable + { + [FieldOffset(0)] + public StencilUid FrontFace; + + [FieldOffset(10)] + public ushort DepthState; + + [FieldOffset(12)] + public StencilUid BackFace; + + [FieldOffset(22)] + private readonly ushort _padding; + + // Quick access aliases +#pragma warning disable IDE0044 // Add readonly modifier + [FieldOffset(0)] + private ulong _id0; + [FieldOffset(8)] + private ulong _id1; + [FieldOffset(0)] + private Vector128 _id01; + [FieldOffset(16)] + private ulong _id2; +#pragma warning restore IDE0044 // Add readonly modifier + + public MTLCompareFunction DepthCompareFunction + { + readonly get => (MTLCompareFunction)((DepthState >> 0) & 0xF); + set => DepthState = (ushort)((DepthState & 0xFFF0) | ((int)value << 0)); + } + + public bool StencilTestEnabled + { + readonly get => ((DepthState >> 4) & 0x1) != 0; + set => DepthState = (ushort)((DepthState & 0xFFEF) | ((value ? 1 : 0) << 4)); + } + + public bool DepthWriteEnabled + { + readonly get => ((DepthState >> 15) & 0x1) != 0; + set => DepthState = (ushort)((DepthState & 0x7FFF) | ((value ? 1 : 0) << 15)); + } + + public readonly override bool Equals(object obj) + { + return obj is DepthStencilUid other && EqualsRef(ref other); + } + + public readonly bool EqualsRef(ref DepthStencilUid other) + { + return _id01.Equals(other._id01) && _id2 == other._id2; + } + + public readonly bool Equals(DepthStencilUid other) + { + return EqualsRef(ref other); + } + + public readonly override int GetHashCode() + { + ulong hash64 = _id0 * 23 ^ + _id1 * 23 ^ + _id2 * 23; + + return (int)hash64 ^ ((int)(hash64 >> 32) * 17); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/State/PipelineState.cs b/src/Ryujinx.Graphics.Metal/State/PipelineState.cs new file mode 100644 index 000000000..9f88f3061 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/State/PipelineState.cs @@ -0,0 +1,341 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.GAL; +using SharpMetal.Foundation; +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + struct PipelineState + { + public PipelineUid Internal; + + public uint StagesCount + { + readonly get => (byte)((Internal.Id0 >> 0) & 0xFF); + set => Internal.Id0 = (Internal.Id0 & 0xFFFFFFFFFFFFFF00) | ((ulong)value << 0); + } + + public uint VertexAttributeDescriptionsCount + { + readonly get => (byte)((Internal.Id0 >> 8) & 0xFF); + set => Internal.Id0 = (Internal.Id0 & 0xFFFFFFFFFFFF00FF) | ((ulong)value << 8); + } + + public uint VertexBindingDescriptionsCount + { + readonly get => (byte)((Internal.Id0 >> 16) & 0xFF); + set => Internal.Id0 = (Internal.Id0 & 0xFFFFFFFFFF00FFFF) | ((ulong)value << 16); + } + + public uint ColorBlendAttachmentStateCount + { + readonly get => (byte)((Internal.Id0 >> 24) & 0xFF); + set => Internal.Id0 = (Internal.Id0 & 0xFFFFFFFF00FFFFFF) | ((ulong)value << 24); + } + + /* + * Can be an input to a pipeline, but not sure what the situation for that is. + public PrimitiveTopology Topology + { + readonly get => (PrimitiveTopology)((Internal.Id6 >> 16) & 0xF); + set => Internal.Id6 = (Internal.Id6 & 0xFFFFFFFFFFF0FFFF) | ((ulong)value << 16); + } + */ + + public MTLLogicOperation LogicOp + { + readonly get => (MTLLogicOperation)((Internal.Id0 >> 32) & 0xF); + set => Internal.Id0 = (Internal.Id0 & 0xFFFFFFF0FFFFFFFF) | ((ulong)value << 32); + } + + //? + public bool PrimitiveRestartEnable + { + readonly get => ((Internal.Id0 >> 36) & 0x1) != 0UL; + set => Internal.Id0 = (Internal.Id0 & 0xFFFFFFEFFFFFFFFF) | ((value ? 1UL : 0UL) << 36); + } + + public bool RasterizerDiscardEnable + { + readonly get => ((Internal.Id0 >> 37) & 0x1) != 0UL; + set => Internal.Id0 = (Internal.Id0 & 0xFFFFFFDFFFFFFFFF) | ((value ? 1UL : 0UL) << 37); + } + + public bool LogicOpEnable + { + readonly get => ((Internal.Id0 >> 38) & 0x1) != 0UL; + set => Internal.Id0 = (Internal.Id0 & 0xFFFFFFBFFFFFFFFF) | ((value ? 1UL : 0UL) << 38); + } + + public bool AlphaToCoverageEnable + { + readonly get => ((Internal.Id0 >> 40) & 0x1) != 0UL; + set => Internal.Id0 = (Internal.Id0 & 0xFFFFFEFFFFFFFFFF) | ((value ? 1UL : 0UL) << 40); + } + + public bool AlphaToOneEnable + { + readonly get => ((Internal.Id0 >> 41) & 0x1) != 0UL; + set => Internal.Id0 = (Internal.Id0 & 0xFFFFFDFFFFFFFFFF) | ((value ? 1UL : 0UL) << 41); + } + + public MTLPixelFormat DepthStencilFormat + { + readonly get => (MTLPixelFormat)(Internal.Id0 >> 48); + set => Internal.Id0 = (Internal.Id0 & 0x0000FFFFFFFFFFFF) | ((ulong)value << 48); + } + + // Not sure how to appropriately use this, but it does need to be passed for tess. + public uint PatchControlPoints + { + readonly get => (uint)((Internal.Id1 >> 0) & 0xFFFFFFFF); + set => Internal.Id1 = (Internal.Id1 & 0xFFFFFFFF00000000) | ((ulong)value << 0); + } + + public uint SamplesCount + { + readonly get => (uint)((Internal.Id1 >> 32) & 0xFFFFFFFF); + set => Internal.Id1 = (Internal.Id1 & 0xFFFFFFFF) | ((ulong)value << 32); + } + + // Advanced blend not supported + + private readonly void BuildColorAttachment(MTLRenderPipelineColorAttachmentDescriptor descriptor, ColorBlendStateUid blendState) + { + descriptor.PixelFormat = blendState.PixelFormat; + descriptor.SetBlendingEnabled(blendState.Enable); + descriptor.AlphaBlendOperation = blendState.AlphaBlendOperation; + descriptor.RgbBlendOperation = blendState.RgbBlendOperation; + descriptor.SourceAlphaBlendFactor = blendState.SourceAlphaBlendFactor; + descriptor.DestinationAlphaBlendFactor = blendState.DestinationAlphaBlendFactor; + descriptor.SourceRGBBlendFactor = blendState.SourceRGBBlendFactor; + descriptor.DestinationRGBBlendFactor = blendState.DestinationRGBBlendFactor; + descriptor.WriteMask = blendState.WriteMask; + } + + private readonly MTLVertexDescriptor BuildVertexDescriptor() + { + var vertexDescriptor = new MTLVertexDescriptor(); + + for (int i = 0; i < VertexAttributeDescriptionsCount; i++) + { + VertexInputAttributeUid uid = Internal.VertexAttributes[i]; + + var attrib = vertexDescriptor.Attributes.Object((ulong)i); + attrib.Format = uid.Format; + attrib.Offset = uid.Offset; + attrib.BufferIndex = uid.BufferIndex; + } + + for (int i = 0; i < VertexBindingDescriptionsCount; i++) + { + VertexInputLayoutUid uid = Internal.VertexBindings[i]; + + var layout = vertexDescriptor.Layouts.Object((ulong)i); + + layout.StepFunction = uid.StepFunction; + layout.StepRate = uid.StepRate; + layout.Stride = uid.Stride; + } + + return vertexDescriptor; + } + + private MTLRenderPipelineDescriptor CreateRenderDescriptor(Program program) + { + var renderPipelineDescriptor = new MTLRenderPipelineDescriptor(); + + for (int i = 0; i < Constants.MaxColorAttachments; i++) + { + var blendState = Internal.ColorBlendState[i]; + + if (blendState.PixelFormat != MTLPixelFormat.Invalid) + { + var pipelineAttachment = renderPipelineDescriptor.ColorAttachments.Object((ulong)i); + + BuildColorAttachment(pipelineAttachment, blendState); + } + } + + MTLPixelFormat dsFormat = DepthStencilFormat; + if (dsFormat != MTLPixelFormat.Invalid) + { + switch (dsFormat) + { + // Depth Only Attachment + case MTLPixelFormat.Depth16Unorm: + case MTLPixelFormat.Depth32Float: + renderPipelineDescriptor.DepthAttachmentPixelFormat = dsFormat; + break; + + // Stencil Only Attachment + case MTLPixelFormat.Stencil8: + renderPipelineDescriptor.StencilAttachmentPixelFormat = dsFormat; + break; + + // Combined Attachment + case MTLPixelFormat.Depth24UnormStencil8: + case MTLPixelFormat.Depth32FloatStencil8: + renderPipelineDescriptor.DepthAttachmentPixelFormat = dsFormat; + renderPipelineDescriptor.StencilAttachmentPixelFormat = dsFormat; + break; + default: + Logger.Error?.PrintMsg(LogClass.Gpu, $"Unsupported Depth/Stencil Format: {dsFormat}!"); + break; + } + } + + renderPipelineDescriptor.LogicOperationEnabled = LogicOpEnable; + renderPipelineDescriptor.LogicOperation = LogicOp; + renderPipelineDescriptor.AlphaToCoverageEnabled = AlphaToCoverageEnable; + renderPipelineDescriptor.AlphaToOneEnabled = AlphaToOneEnable; + renderPipelineDescriptor.RasterizationEnabled = !RasterizerDiscardEnable; + renderPipelineDescriptor.SampleCount = Math.Max(1, SamplesCount); + + var vertexDescriptor = BuildVertexDescriptor(); + renderPipelineDescriptor.VertexDescriptor = vertexDescriptor; + + renderPipelineDescriptor.VertexFunction = program.VertexFunction; + + if (program.FragmentFunction.NativePtr != 0) + { + renderPipelineDescriptor.FragmentFunction = program.FragmentFunction; + } + + return renderPipelineDescriptor; + } + + public MTLRenderPipelineState CreateRenderPipeline(MTLDevice device, Program program) + { + if (program.TryGetGraphicsPipeline(ref Internal, out var pipelineState)) + { + return pipelineState; + } + + using var descriptor = CreateRenderDescriptor(program); + + var error = new NSError(IntPtr.Zero); + pipelineState = device.NewRenderPipelineState(descriptor, ref error); + if (error != IntPtr.Zero) + { + Logger.Error?.PrintMsg(LogClass.Gpu, $"Failed to create Render Pipeline State: {StringHelper.String(error.LocalizedDescription)}"); + } + + program.AddGraphicsPipeline(ref Internal, pipelineState); + + return pipelineState; + } + + public static MTLComputePipelineDescriptor CreateComputeDescriptor(Program program) + { + ComputeSize localSize = program.ComputeLocalSize; + + uint maxThreads = (uint)(localSize.X * localSize.Y * localSize.Z); + + if (maxThreads == 0) + { + throw new InvalidOperationException($"Local thread size for compute cannot be 0 in any dimension."); + } + + var descriptor = new MTLComputePipelineDescriptor + { + ComputeFunction = program.ComputeFunction, + MaxTotalThreadsPerThreadgroup = maxThreads, + ThreadGroupSizeIsMultipleOfThreadExecutionWidth = true, + }; + + return descriptor; + } + + public static MTLComputePipelineState CreateComputePipeline(MTLDevice device, Program program) + { + if (program.TryGetComputePipeline(out var pipelineState)) + { + return pipelineState; + } + + using MTLComputePipelineDescriptor descriptor = CreateComputeDescriptor(program); + + var error = new NSError(IntPtr.Zero); + pipelineState = device.NewComputePipelineState(descriptor, MTLPipelineOption.None, 0, ref error); + if (error != IntPtr.Zero) + { + Logger.Error?.PrintMsg(LogClass.Gpu, $"Failed to create Compute Pipeline State: {StringHelper.String(error.LocalizedDescription)}"); + } + + program.AddComputePipeline(pipelineState); + + return pipelineState; + } + + public void Initialize() + { + SamplesCount = 1; + + Internal.ResetColorState(); + } + + /* + * TODO, this is from vulkan. + + private void UpdateVertexAttributeDescriptions(VulkanRenderer gd) + { + // Vertex attributes exceeding the stride are invalid. + // In metal, they cause glitches with the vertex shader fetching incorrect values. + // To work around this, we reduce the format to something that doesn't exceed the stride if possible. + // The assumption is that the exceeding components are not actually accessed on the shader. + + for (int index = 0; index < VertexAttributeDescriptionsCount; index++) + { + var attribute = Internal.VertexAttributeDescriptions[index]; + int vbIndex = GetVertexBufferIndex(attribute.Binding); + + if (vbIndex >= 0) + { + ref var vb = ref Internal.VertexBindingDescriptions[vbIndex]; + + Format format = attribute.Format; + + while (vb.Stride != 0 && attribute.Offset + FormatTable.GetAttributeFormatSize(format) > vb.Stride) + { + Format newFormat = FormatTable.DropLastComponent(format); + + if (newFormat == format) + { + // That case means we failed to find a format that fits within the stride, + // so just restore the original format and give up. + format = attribute.Format; + break; + } + + format = newFormat; + } + + if (attribute.Format != format && gd.FormatCapabilities.BufferFormatSupports(FormatFeatureFlags.VertexBufferBit, format)) + { + attribute.Format = format; + } + } + + _vertexAttributeDescriptions2[index] = attribute; + } + } + + private int GetVertexBufferIndex(uint binding) + { + for (int index = 0; index < VertexBindingDescriptionsCount; index++) + { + if (Internal.VertexBindingDescriptions[index].Binding == binding) + { + return index; + } + } + + return -1; + } + */ + } +} diff --git a/src/Ryujinx.Graphics.Metal/State/PipelineUid.cs b/src/Ryujinx.Graphics.Metal/State/PipelineUid.cs new file mode 100644 index 000000000..c986a7e23 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/State/PipelineUid.cs @@ -0,0 +1,208 @@ +using Ryujinx.Common.Memory; +using SharpMetal.Metal; +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + struct VertexInputAttributeUid + { + public ulong Id0; + + public ulong Offset + { + readonly get => (uint)((Id0 >> 0) & 0xFFFFFFFF); + set => Id0 = (Id0 & 0xFFFFFFFF00000000) | ((ulong)value << 0); + } + + public MTLVertexFormat Format + { + readonly get => (MTLVertexFormat)((Id0 >> 32) & 0xFFFF); + set => Id0 = (Id0 & 0xFFFF0000FFFFFFFF) | ((ulong)value << 32); + } + + public ulong BufferIndex + { + readonly get => ((Id0 >> 48) & 0xFFFF); + set => Id0 = (Id0 & 0x0000FFFFFFFFFFFF) | ((ulong)value << 48); + } + } + + struct VertexInputLayoutUid + { + public ulong Id0; + + public uint Stride + { + readonly get => (uint)((Id0 >> 0) & 0xFFFFFFFF); + set => Id0 = (Id0 & 0xFFFFFFFF00000000) | ((ulong)value << 0); + } + + public uint StepRate + { + readonly get => (uint)((Id0 >> 32) & 0x1FFFFFFF); + set => Id0 = (Id0 & 0xE0000000FFFFFFFF) | ((ulong)value << 32); + } + + public MTLVertexStepFunction StepFunction + { + readonly get => (MTLVertexStepFunction)((Id0 >> 61) & 0x7); + set => Id0 = (Id0 & 0x1FFFFFFFFFFFFFFF) | ((ulong)value << 61); + } + } + + struct ColorBlendStateUid + { + public ulong Id0; + + public MTLPixelFormat PixelFormat + { + readonly get => (MTLPixelFormat)((Id0 >> 0) & 0xFFFF); + set => Id0 = (Id0 & 0xFFFFFFFFFFFF0000) | ((ulong)value << 0); + } + + public MTLBlendFactor SourceRGBBlendFactor + { + readonly get => (MTLBlendFactor)((Id0 >> 16) & 0xFF); + set => Id0 = (Id0 & 0xFFFFFFFFFF00FFFF) | ((ulong)value << 16); + } + + public MTLBlendFactor DestinationRGBBlendFactor + { + readonly get => (MTLBlendFactor)((Id0 >> 24) & 0xFF); + set => Id0 = (Id0 & 0xFFFFFFFF00FFFFFF) | ((ulong)value << 24); + } + + public MTLBlendOperation RgbBlendOperation + { + readonly get => (MTLBlendOperation)((Id0 >> 32) & 0xF); + set => Id0 = (Id0 & 0xFFFFFFF0FFFFFFFF) | ((ulong)value << 32); + } + + public MTLBlendOperation AlphaBlendOperation + { + readonly get => (MTLBlendOperation)((Id0 >> 36) & 0xF); + set => Id0 = (Id0 & 0xFFFFFF0FFFFFFFFF) | ((ulong)value << 36); + } + + public MTLBlendFactor SourceAlphaBlendFactor + { + readonly get => (MTLBlendFactor)((Id0 >> 40) & 0xFF); + set => Id0 = (Id0 & 0xFFFF00FFFFFFFFFF) | ((ulong)value << 40); + } + + public MTLBlendFactor DestinationAlphaBlendFactor + { + readonly get => (MTLBlendFactor)((Id0 >> 48) & 0xFF); + set => Id0 = (Id0 & 0xFF00FFFFFFFFFFFF) | ((ulong)value << 48); + } + + public MTLColorWriteMask WriteMask + { + readonly get => (MTLColorWriteMask)((Id0 >> 56) & 0xF); + set => Id0 = (Id0 & 0xF0FFFFFFFFFFFFFF) | ((ulong)value << 56); + } + + public bool Enable + { + readonly get => ((Id0 >> 63) & 0x1) != 0UL; + set => Id0 = (Id0 & 0x7FFFFFFFFFFFFFFF) | ((value ? 1UL : 0UL) << 63); + } + + public void Swap(ColorBlendStateUid uid) + { + var format = PixelFormat; + + this = uid; + PixelFormat = format; + } + } + + [SupportedOSPlatform("macos")] + struct PipelineUid : IRefEquatable + { + public ulong Id0; + public ulong Id1; + + private readonly uint VertexAttributeDescriptionsCount => (byte)((Id0 >> 8) & 0xFF); + private readonly uint VertexBindingDescriptionsCount => (byte)((Id0 >> 16) & 0xFF); + private readonly uint ColorBlendAttachmentStateCount => (byte)((Id0 >> 24) & 0xFF); + + public Array32 VertexAttributes; + public Array33 VertexBindings; + public Array8 ColorBlendState; + public uint AttachmentIntegerFormatMask; + public bool LogicOpsAllowed; + + public void ResetColorState() + { + ColorBlendState = new(); + + for (int i = 0; i < ColorBlendState.Length; i++) + { + ColorBlendState[i].WriteMask = MTLColorWriteMask.All; + } + } + + public readonly override bool Equals(object obj) + { + return obj is PipelineUid other && Equals(other); + } + + public bool Equals(ref PipelineUid other) + { + if (!Unsafe.As>(ref Id0).Equals(Unsafe.As>(ref other.Id0))) + { + return false; + } + + if (!SequenceEqual(VertexAttributes.AsSpan(), other.VertexAttributes.AsSpan(), VertexAttributeDescriptionsCount)) + { + return false; + } + + if (!SequenceEqual(VertexBindings.AsSpan(), other.VertexBindings.AsSpan(), VertexBindingDescriptionsCount)) + { + return false; + } + + if (!SequenceEqual(ColorBlendState.AsSpan(), other.ColorBlendState.AsSpan(), ColorBlendAttachmentStateCount)) + { + return false; + } + + return true; + } + + private static bool SequenceEqual(ReadOnlySpan x, ReadOnlySpan y, uint count) where T : unmanaged + { + return MemoryMarshal.Cast(x[..(int)count]).SequenceEqual(MemoryMarshal.Cast(y[..(int)count])); + } + + public override int GetHashCode() + { + ulong hash64 = Id0 * 23 ^ + Id1 * 23; + + for (int i = 0; i < (int)VertexAttributeDescriptionsCount; i++) + { + hash64 ^= VertexAttributes[i].Id0 * 23; + } + + for (int i = 0; i < (int)VertexBindingDescriptionsCount; i++) + { + hash64 ^= VertexBindings[i].Id0 * 23; + } + + for (int i = 0; i < (int)ColorBlendAttachmentStateCount; i++) + { + hash64 ^= ColorBlendState[i].Id0 * 23; + } + + return (int)hash64 ^ ((int)(hash64 >> 32) * 17); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/StateCache.cs b/src/Ryujinx.Graphics.Metal/StateCache.cs new file mode 100644 index 000000000..9b8391ffc --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/StateCache.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + abstract class StateCache : IDisposable where T : IDisposable + { + private readonly Dictionary _cache = new(); + + protected abstract THash GetHash(TDescriptor descriptor); + + protected abstract T CreateValue(TDescriptor descriptor); + + public void Dispose() + { + foreach (T value in _cache.Values) + { + value.Dispose(); + } + + GC.SuppressFinalize(this); + } + + public T GetOrCreate(TDescriptor descriptor) + { + var hash = GetHash(descriptor); + if (_cache.TryGetValue(hash, out T value)) + { + return value; + } + else + { + var newValue = CreateValue(descriptor); + _cache.Add(hash, newValue); + + return newValue; + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/StringHelper.cs b/src/Ryujinx.Graphics.Metal/StringHelper.cs new file mode 100644 index 000000000..46e8ad2e9 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/StringHelper.cs @@ -0,0 +1,30 @@ +using SharpMetal.Foundation; +using SharpMetal.ObjectiveCCore; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + class StringHelper + { + public static NSString NSString(string source) + { + return new(ObjectiveC.IntPtr_objc_msgSend(new ObjectiveCClass("NSString"), "stringWithUTF8String:", source)); + } + + public static unsafe string String(NSString source) + { + char[] sourceBuffer = new char[source.Length]; + fixed (char* pSourceBuffer = sourceBuffer) + { + ObjectiveC.bool_objc_msgSend(source, + "getCString:maxLength:encoding:", + pSourceBuffer, + source.MaximumLengthOfBytes(NSStringEncoding.UTF16) + 1, + (ulong)NSStringEncoding.UTF16); + } + + return new string(sourceBuffer); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/SyncManager.cs b/src/Ryujinx.Graphics.Metal/SyncManager.cs new file mode 100644 index 000000000..ca49fe263 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/SyncManager.cs @@ -0,0 +1,214 @@ +using Ryujinx.Common.Logging; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + class SyncManager + { + private class SyncHandle + { + public ulong ID; + public MultiFenceHolder Waitable; + public ulong FlushId; + public bool Signalled; + + public bool NeedsFlush(ulong currentFlushId) + { + return (long)(FlushId - currentFlushId) >= 0; + } + } + + private ulong _firstHandle; + + private readonly MetalRenderer _renderer; + private readonly List _handles; + private ulong _flushId; + private long _waitTicks; + + public SyncManager(MetalRenderer renderer) + { + _renderer = renderer; + _handles = new List(); + } + + public void RegisterFlush() + { + _flushId++; + } + + public void Create(ulong id, bool strict) + { + ulong flushId = _flushId; + MultiFenceHolder waitable = new(); + if (strict || _renderer.InterruptAction == null) + { + _renderer.FlushAllCommands(); + _renderer.CommandBufferPool.AddWaitable(waitable); + } + else + { + // Don't flush commands, instead wait for the current command buffer to finish. + // If this sync is waited on before the command buffer is submitted, interrupt the gpu thread and flush it manually. + + _renderer.CommandBufferPool.AddInUseWaitable(waitable); + } + + SyncHandle handle = new() + { + ID = id, + Waitable = waitable, + FlushId = flushId, + }; + + lock (_handles) + { + _handles.Add(handle); + } + } + + public ulong GetCurrent() + { + lock (_handles) + { + ulong lastHandle = _firstHandle; + + foreach (SyncHandle handle in _handles) + { + lock (handle) + { + if (handle.Waitable == null) + { + continue; + } + + if (handle.ID > lastHandle) + { + bool signaled = handle.Signalled || handle.Waitable.WaitForFences(false); + if (signaled) + { + lastHandle = handle.ID; + handle.Signalled = true; + } + } + } + } + + return lastHandle; + } + } + + public void Wait(ulong id) + { + SyncHandle result = null; + + lock (_handles) + { + if ((long)(_firstHandle - id) > 0) + { + return; // The handle has already been signalled or deleted. + } + + foreach (SyncHandle handle in _handles) + { + if (handle.ID == id) + { + result = handle; + break; + } + } + } + + if (result != null) + { + if (result.Waitable == null) + { + return; + } + + long beforeTicks = Stopwatch.GetTimestamp(); + + if (result.NeedsFlush(_flushId)) + { + _renderer.InterruptAction(() => + { + if (result.NeedsFlush(_flushId)) + { + _renderer.FlushAllCommands(); + } + }); + } + + lock (result) + { + if (result.Waitable == null) + { + return; + } + + bool signaled = result.Signalled || result.Waitable.WaitForFences(true); + + if (!signaled) + { + Logger.Error?.PrintMsg(LogClass.Gpu, $"Metal Sync Object {result.ID} failed to signal within 1000ms. Continuing..."); + } + else + { + _waitTicks += Stopwatch.GetTimestamp() - beforeTicks; + result.Signalled = true; + } + } + } + } + + public void Cleanup() + { + // Iterate through handles and remove any that have already been signalled. + + while (true) + { + SyncHandle first = null; + lock (_handles) + { + first = _handles.FirstOrDefault(); + } + + if (first == null || first.NeedsFlush(_flushId)) + { + break; + } + + bool signaled = first.Waitable.WaitForFences(false); + if (signaled) + { + // Delete the sync object. + lock (_handles) + { + lock (first) + { + _firstHandle = first.ID + 1; + _handles.RemoveAt(0); + first.Waitable = null; + } + } + } + else + { + // This sync handle and any following have not been reached yet. + break; + } + } + } + + public long GetAndResetWaitTicks() + { + long result = _waitTicks; + _waitTicks = 0; + + return result; + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/Texture.cs b/src/Ryujinx.Graphics.Metal/Texture.cs new file mode 100644 index 000000000..4566d65d8 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Texture.cs @@ -0,0 +1,654 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Common.Memory; +using Ryujinx.Graphics.GAL; +using SharpMetal.Foundation; +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + class Texture : TextureBase, ITexture + { + private MTLTexture _identitySwizzleHandle; + private readonly bool _identityIsDifferent; + + public Texture(MTLDevice device, MetalRenderer renderer, Pipeline pipeline, TextureCreateInfo info) : base(device, renderer, pipeline, info) + { + MTLPixelFormat pixelFormat = FormatTable.GetFormat(Info.Format); + + var descriptor = new MTLTextureDescriptor + { + PixelFormat = pixelFormat, + Usage = MTLTextureUsage.Unknown, + SampleCount = (ulong)Info.Samples, + TextureType = Info.Target.Convert(), + Width = (ulong)Info.Width, + Height = (ulong)Info.Height, + MipmapLevelCount = (ulong)Info.Levels + }; + + if (info.Target == Target.Texture3D) + { + descriptor.Depth = (ulong)Info.Depth; + } + else if (info.Target != Target.Cubemap) + { + if (info.Target == Target.CubemapArray) + { + descriptor.ArrayLength = (ulong)(Info.Depth / 6); + } + else + { + descriptor.ArrayLength = (ulong)Info.Depth; + } + } + + MTLTextureSwizzleChannels swizzle = GetSwizzle(info, descriptor.PixelFormat); + + _identitySwizzleHandle = Device.NewTexture(descriptor); + + if (SwizzleIsIdentity(swizzle)) + { + MtlTexture = _identitySwizzleHandle; + } + else + { + MtlTexture = CreateDefaultView(_identitySwizzleHandle, swizzle, descriptor); + _identityIsDifferent = true; + } + + MtlFormat = pixelFormat; + descriptor.Dispose(); + } + + public Texture(MTLDevice device, MetalRenderer renderer, Pipeline pipeline, TextureCreateInfo info, MTLTexture sourceTexture, int firstLayer, int firstLevel) : base(device, renderer, pipeline, info) + { + var pixelFormat = FormatTable.GetFormat(Info.Format); + + if (info.DepthStencilMode == DepthStencilMode.Stencil) + { + pixelFormat = pixelFormat switch + { + MTLPixelFormat.Depth32FloatStencil8 => MTLPixelFormat.X32Stencil8, + MTLPixelFormat.Depth24UnormStencil8 => MTLPixelFormat.X24Stencil8, + _ => pixelFormat + }; + } + + var textureType = Info.Target.Convert(); + NSRange levels; + levels.location = (ulong)firstLevel; + levels.length = (ulong)Info.Levels; + NSRange slices; + slices.location = (ulong)firstLayer; + slices.length = textureType == MTLTextureType.Type3D ? 1 : (ulong)info.GetDepthOrLayers(); + + var swizzle = GetSwizzle(info, pixelFormat); + + _identitySwizzleHandle = sourceTexture.NewTextureView(pixelFormat, textureType, levels, slices); + + if (SwizzleIsIdentity(swizzle)) + { + MtlTexture = _identitySwizzleHandle; + } + else + { + MtlTexture = sourceTexture.NewTextureView(pixelFormat, textureType, levels, slices, swizzle); + _identityIsDifferent = true; + } + + MtlFormat = pixelFormat; + FirstLayer = firstLayer; + FirstLevel = firstLevel; + } + + public void PopulateRenderPassAttachment(MTLRenderPassColorAttachmentDescriptor descriptor) + { + descriptor.Texture = _identitySwizzleHandle; + } + + private MTLTexture CreateDefaultView(MTLTexture texture, MTLTextureSwizzleChannels swizzle, MTLTextureDescriptor descriptor) + { + NSRange levels; + levels.location = 0; + levels.length = (ulong)Info.Levels; + NSRange slices; + slices.location = 0; + slices.length = Info.Target == Target.Texture3D ? 1 : (ulong)Info.GetDepthOrLayers(); + + return texture.NewTextureView(descriptor.PixelFormat, descriptor.TextureType, levels, slices, swizzle); + } + + private bool SwizzleIsIdentity(MTLTextureSwizzleChannels swizzle) + { + return swizzle.red == MTLTextureSwizzle.Red && + swizzle.green == MTLTextureSwizzle.Green && + swizzle.blue == MTLTextureSwizzle.Blue && + swizzle.alpha == MTLTextureSwizzle.Alpha; + } + + private MTLTextureSwizzleChannels GetSwizzle(TextureCreateInfo info, MTLPixelFormat pixelFormat) + { + var swizzleR = Info.SwizzleR.Convert(); + var swizzleG = Info.SwizzleG.Convert(); + var swizzleB = Info.SwizzleB.Convert(); + var swizzleA = Info.SwizzleA.Convert(); + + if (info.Format == Format.R5G5B5A1Unorm || + info.Format == Format.R5G5B5X1Unorm || + info.Format == Format.R5G6B5Unorm) + { + (swizzleB, swizzleR) = (swizzleR, swizzleB); + } + else if (pixelFormat == MTLPixelFormat.ABGR4Unorm || info.Format == Format.A1B5G5R5Unorm) + { + var tempB = swizzleB; + var tempA = swizzleA; + + swizzleB = swizzleG; + swizzleA = swizzleR; + swizzleR = tempA; + swizzleG = tempB; + } + + return new MTLTextureSwizzleChannels + { + red = swizzleR, + green = swizzleG, + blue = swizzleB, + alpha = swizzleA + }; + } + + public void CopyTo(ITexture destination, int firstLayer, int firstLevel) + { + CommandBufferScoped cbs = Pipeline.Cbs; + + TextureBase src = this; + TextureBase dst = (TextureBase)destination; + + if (!Valid || !dst.Valid) + { + return; + } + + var srcImage = GetHandle(); + var dstImage = dst.GetHandle(); + + if (!dst.Info.Target.IsMultisample() && Info.Target.IsMultisample()) + { + // int layers = Math.Min(Info.GetLayers(), dst.Info.GetLayers() - firstLayer); + + // _gd.HelperShader.CopyMSToNonMS(_gd, cbs, src, dst, 0, firstLayer, layers); + } + else if (dst.Info.Target.IsMultisample() && !Info.Target.IsMultisample()) + { + // int layers = Math.Min(Info.GetLayers(), dst.Info.GetLayers() - firstLayer); + + // _gd.HelperShader.CopyNonMSToMS(_gd, cbs, src, dst, 0, firstLayer, layers); + } + else if (dst.Info.BytesPerPixel != Info.BytesPerPixel) + { + // int layers = Math.Min(Info.GetLayers(), dst.Info.GetLayers() - firstLayer); + // int levels = Math.Min(Info.Levels, dst.Info.Levels - firstLevel); + + // _gd.HelperShader.CopyIncompatibleFormats(_gd, cbs, src, dst, 0, firstLayer, 0, firstLevel, layers, levels); + } + else if (src.Info.Format.IsDepthOrStencil() != dst.Info.Format.IsDepthOrStencil()) + { + // int layers = Math.Min(Info.GetLayers(), dst.Info.GetLayers() - firstLayer); + // int levels = Math.Min(Info.Levels, dst.Info.Levels - firstLevel); + + // TODO: depth copy? + // _gd.HelperShader.CopyColor(_gd, cbs, src, dst, 0, firstLayer, 0, FirstLevel, layers, levels); + } + else + { + TextureCopy.Copy( + cbs, + srcImage, + dstImage, + src.Info, + dst.Info, + 0, + firstLayer, + 0, + firstLevel); + } + } + + public void CopyTo(ITexture destination, int srcLayer, int dstLayer, int srcLevel, int dstLevel) + { + CommandBufferScoped cbs = Pipeline.Cbs; + + TextureBase src = this; + TextureBase dst = (TextureBase)destination; + + if (!Valid || !dst.Valid) + { + return; + } + + var srcImage = GetHandle(); + var dstImage = dst.GetHandle(); + + if (!dst.Info.Target.IsMultisample() && Info.Target.IsMultisample()) + { + // _gd.HelperShader.CopyMSToNonMS(_gd, cbs, src, dst, srcLayer, dstLayer, 1); + } + else if (dst.Info.Target.IsMultisample() && !Info.Target.IsMultisample()) + { + // _gd.HelperShader.CopyNonMSToMS(_gd, cbs, src, dst, srcLayer, dstLayer, 1); + } + else if (dst.Info.BytesPerPixel != Info.BytesPerPixel) + { + // _gd.HelperShader.CopyIncompatibleFormats(_gd, cbs, src, dst, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1); + } + else if (src.Info.Format.IsDepthOrStencil() != dst.Info.Format.IsDepthOrStencil()) + { + // _gd.HelperShader.CopyColor(_gd, cbs, src, dst, srcLayer, dstLayer, srcLevel, dstLevel, 1, 1); + } + else + { + TextureCopy.Copy( + cbs, + srcImage, + dstImage, + src.Info, + dst.Info, + srcLayer, + dstLayer, + srcLevel, + dstLevel, + 1, + 1); + } + } + + public void CopyTo(ITexture destination, Extents2D srcRegion, Extents2D dstRegion, bool linearFilter) + { + if (!Renderer.CommandBufferPool.OwnedByCurrentThread) + { + Logger.Warning?.PrintMsg(LogClass.Gpu, "Metal doesn't currently support scaled blit on background thread."); + + return; + } + + var dst = (Texture)destination; + + bool isDepthOrStencil = dst.Info.Format.IsDepthOrStencil(); + + Pipeline.Blit(this, dst, srcRegion, dstRegion, isDepthOrStencil, linearFilter); + } + + public void CopyTo(BufferRange range, int layer, int level, int stride) + { + var cbs = Pipeline.Cbs; + + int outSize = Info.GetMipSize(level); + int hostSize = GetBufferDataLength(outSize); + + int offset = range.Offset; + + var autoBuffer = Renderer.BufferManager.GetBuffer(range.Handle, true); + var mtlBuffer = autoBuffer.Get(cbs, range.Offset, outSize).Value; + + if (PrepareOutputBuffer(cbs, hostSize, mtlBuffer, out MTLBuffer copyToBuffer, out BufferHolder tempCopyHolder)) + { + offset = 0; + } + + CopyFromOrToBuffer(cbs, copyToBuffer, MtlTexture, hostSize, true, layer, level, 1, 1, singleSlice: true, offset, stride); + + if (tempCopyHolder != null) + { + CopyDataToOutputBuffer(cbs, tempCopyHolder, autoBuffer, hostSize, range.Offset); + tempCopyHolder.Dispose(); + } + } + + public ITexture CreateView(TextureCreateInfo info, int firstLayer, int firstLevel) + { + return new Texture(Device, Renderer, Pipeline, info, _identitySwizzleHandle, firstLayer, firstLevel); + } + + private void CopyDataToBuffer(Span storage, ReadOnlySpan input) + { + if (NeedsD24S8Conversion()) + { + FormatConverter.ConvertD24S8ToD32FS8(storage, input); + return; + } + + input.CopyTo(storage); + } + + private ReadOnlySpan GetDataFromBuffer(ReadOnlySpan storage, int size, Span output) + { + if (NeedsD24S8Conversion()) + { + if (output.IsEmpty) + { + output = new byte[GetBufferDataLength(size)]; + } + + FormatConverter.ConvertD32FS8ToD24S8(output, storage); + return output; + } + + return storage; + } + + private bool PrepareOutputBuffer(CommandBufferScoped cbs, int hostSize, MTLBuffer target, out MTLBuffer copyTarget, out BufferHolder copyTargetHolder) + { + if (NeedsD24S8Conversion()) + { + copyTargetHolder = Renderer.BufferManager.Create(hostSize); + copyTarget = copyTargetHolder.GetBuffer().Get(cbs, 0, hostSize).Value; + + return true; + } + + copyTarget = target; + copyTargetHolder = null; + + return false; + } + + private void CopyDataToOutputBuffer(CommandBufferScoped cbs, BufferHolder hostData, Auto copyTarget, int hostSize, int dstOffset) + { + if (NeedsD24S8Conversion()) + { + Renderer.HelperShader.ConvertD32S8ToD24S8(cbs, hostData, copyTarget, hostSize / (2 * sizeof(int)), dstOffset); + } + } + + private bool NeedsD24S8Conversion() + { + return FormatTable.IsD24S8(Info.Format) && MtlFormat == MTLPixelFormat.Depth32FloatStencil8; + } + + public void CopyFromOrToBuffer( + CommandBufferScoped cbs, + MTLBuffer buffer, + MTLTexture image, + int size, + bool to, + int dstLayer, + int dstLevel, + int dstLayers, + int dstLevels, + bool singleSlice, + int offset = 0, + int stride = 0) + { + MTLBlitCommandEncoder blitCommandEncoder = cbs.Encoders.EnsureBlitEncoder(); + + bool is3D = Info.Target == Target.Texture3D; + int width = Math.Max(1, Info.Width >> dstLevel); + int height = Math.Max(1, Info.Height >> dstLevel); + int depth = is3D && !singleSlice ? Math.Max(1, Info.Depth >> dstLevel) : 1; + int layers = dstLayers; + int levels = dstLevels; + + for (int oLevel = 0; oLevel < levels; oLevel++) + { + int level = oLevel + dstLevel; + int mipSize = Info.GetMipSize2D(level); + + int mipSizeLevel = GetBufferDataLength(is3D && !singleSlice + ? Info.GetMipSize(level) + : mipSize * dstLayers); + + int endOffset = offset + mipSizeLevel; + + if ((uint)endOffset > (uint)size) + { + break; + } + + for (int oLayer = 0; oLayer < layers; oLayer++) + { + int layer = !is3D ? dstLayer + oLayer : 0; + int z = is3D ? dstLayer + oLayer : 0; + + if (to) + { + blitCommandEncoder.CopyFromTexture( + image, + (ulong)layer, + (ulong)level, + new MTLOrigin { z = (ulong)z }, + new MTLSize { width = (ulong)width, height = (ulong)height, depth = 1 }, + buffer, + (ulong)offset, + (ulong)Info.GetMipStride(level), + (ulong)mipSize + ); + } + else + { + blitCommandEncoder.CopyFromBuffer( + buffer, + (ulong)offset, + (ulong)Info.GetMipStride(level), + (ulong)mipSize, + new MTLSize { width = (ulong)width, height = (ulong)height, depth = 1 }, + image, + (ulong)(layer + oLayer), + (ulong)level, + new MTLOrigin { z = (ulong)z } + ); + } + + offset += mipSize; + } + + width = Math.Max(1, width >> 1); + height = Math.Max(1, height >> 1); + + if (Info.Target == Target.Texture3D) + { + depth = Math.Max(1, depth >> 1); + } + } + } + + private ReadOnlySpan GetData(CommandBufferPool cbp, PersistentFlushBuffer flushBuffer) + { + int size = 0; + + for (int level = 0; level < Info.Levels; level++) + { + size += Info.GetMipSize(level); + } + + size = GetBufferDataLength(size); + + Span result = flushBuffer.GetTextureData(cbp, this, size); + + return GetDataFromBuffer(result, size, result); + } + + private ReadOnlySpan GetData(CommandBufferPool cbp, PersistentFlushBuffer flushBuffer, int layer, int level) + { + int size = GetBufferDataLength(Info.GetMipSize(level)); + + Span result = flushBuffer.GetTextureData(cbp, this, size, layer, level); + + return GetDataFromBuffer(result, size, result); + } + + public PinnedSpan GetData() + { + BackgroundResource resources = Renderer.BackgroundResources.Get(); + + if (Renderer.CommandBufferPool.OwnedByCurrentThread) + { + Renderer.FlushAllCommands(); + + return PinnedSpan.UnsafeFromSpan(GetData(Renderer.CommandBufferPool, resources.GetFlushBuffer())); + } + + return PinnedSpan.UnsafeFromSpan(GetData(resources.GetPool(), resources.GetFlushBuffer())); + } + + public PinnedSpan GetData(int layer, int level) + { + BackgroundResource resources = Renderer.BackgroundResources.Get(); + + if (Renderer.CommandBufferPool.OwnedByCurrentThread) + { + Renderer.FlushAllCommands(); + + return PinnedSpan.UnsafeFromSpan(GetData(Renderer.CommandBufferPool, resources.GetFlushBuffer(), layer, level)); + } + + return PinnedSpan.UnsafeFromSpan(GetData(resources.GetPool(), resources.GetFlushBuffer(), layer, level)); + } + + public void SetData(MemoryOwner data) + { + var blitCommandEncoder = Pipeline.GetOrCreateBlitEncoder(); + + var dataSpan = data.Memory.Span; + + var buffer = Renderer.BufferManager.Create(dataSpan.Length); + buffer.SetDataUnchecked(0, dataSpan); + var mtlBuffer = buffer.GetBuffer(false).Get(Pipeline.Cbs).Value; + + int width = Info.Width; + int height = Info.Height; + int depth = Info.Depth; + int levels = Info.Levels; + int layers = Info.GetLayers(); + bool is3D = Info.Target == Target.Texture3D; + + int offset = 0; + + for (int level = 0; level < levels; level++) + { + int mipSize = Info.GetMipSize2D(level); + int endOffset = offset + mipSize; + + if ((uint)endOffset > (uint)dataSpan.Length) + { + return; + } + + for (int layer = 0; layer < layers; layer++) + { + blitCommandEncoder.CopyFromBuffer( + mtlBuffer, + (ulong)offset, + (ulong)Info.GetMipStride(level), + (ulong)mipSize, + new MTLSize { width = (ulong)width, height = (ulong)height, depth = is3D ? (ulong)depth : 1 }, + MtlTexture, + (ulong)layer, + (ulong)level, + new MTLOrigin() + ); + + offset += mipSize; + } + + width = Math.Max(1, width >> 1); + height = Math.Max(1, height >> 1); + + if (is3D) + { + depth = Math.Max(1, depth >> 1); + } + } + + // Cleanup + buffer.Dispose(); + } + + private void SetData(ReadOnlySpan data, int layer, int level, int layers, int levels, bool singleSlice) + { + int bufferDataLength = GetBufferDataLength(data.Length); + + using var bufferHolder = Renderer.BufferManager.Create(bufferDataLength); + + // TODO: loadInline logic + + var cbs = Pipeline.Cbs; + + CopyDataToBuffer(bufferHolder.GetDataStorage(0, bufferDataLength), data); + + var buffer = bufferHolder.GetBuffer().Get(cbs).Value; + var image = GetHandle(); + + CopyFromOrToBuffer(cbs, buffer, image, bufferDataLength, false, layer, level, layers, levels, singleSlice); + } + + public void SetData(MemoryOwner data, int layer, int level) + { + SetData(data.Memory.Span, layer, level, 1, 1, singleSlice: true); + + data.Dispose(); + } + + public void SetData(MemoryOwner data, int layer, int level, Rectangle region) + { + var blitCommandEncoder = Pipeline.GetOrCreateBlitEncoder(); + + ulong bytesPerRow = (ulong)Info.GetMipStride(level); + ulong bytesPerImage = 0; + if (MtlTexture.TextureType == MTLTextureType.Type3D) + { + bytesPerImage = bytesPerRow * (ulong)Info.Height; + } + + var dataSpan = data.Memory.Span; + + var buffer = Renderer.BufferManager.Create(dataSpan.Length); + buffer.SetDataUnchecked(0, dataSpan); + var mtlBuffer = buffer.GetBuffer(false).Get(Pipeline.Cbs).Value; + + blitCommandEncoder.CopyFromBuffer( + mtlBuffer, + 0, + bytesPerRow, + bytesPerImage, + new MTLSize { width = (ulong)region.Width, height = (ulong)region.Height, depth = 1 }, + MtlTexture, + (ulong)layer, + (ulong)level, + new MTLOrigin { x = (ulong)region.X, y = (ulong)region.Y } + ); + + // Cleanup + buffer.Dispose(); + } + + private int GetBufferDataLength(int length) + { + if (NeedsD24S8Conversion()) + { + return length * 2; + } + + return length; + } + + public void SetStorage(BufferRange buffer) + { + throw new NotImplementedException(); + } + + public override void Release() + { + if (_identityIsDifferent) + { + _identitySwizzleHandle.Dispose(); + } + + base.Release(); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/TextureArray.cs b/src/Ryujinx.Graphics.Metal/TextureArray.cs new file mode 100644 index 000000000..ea2c74420 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/TextureArray.cs @@ -0,0 +1,93 @@ +using Ryujinx.Graphics.GAL; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + internal class TextureArray : ITextureArray + { + private readonly TextureRef[] _textureRefs; + private readonly TextureBuffer[] _bufferTextureRefs; + + private readonly bool _isBuffer; + private readonly Pipeline _pipeline; + + public TextureArray(int size, bool isBuffer, Pipeline pipeline) + { + if (isBuffer) + { + _bufferTextureRefs = new TextureBuffer[size]; + } + else + { + _textureRefs = new TextureRef[size]; + } + + _isBuffer = isBuffer; + _pipeline = pipeline; + } + + public void SetSamplers(int index, ISampler[] samplers) + { + for (int i = 0; i < samplers.Length; i++) + { + ISampler sampler = samplers[i]; + + if (sampler is SamplerHolder samp) + { + _textureRefs[index + i].Sampler = samp.GetSampler(); + } + else + { + _textureRefs[index + i].Sampler = default; + } + } + + SetDirty(); + } + + public void SetTextures(int index, ITexture[] textures) + { + for (int i = 0; i < textures.Length; i++) + { + ITexture texture = textures[i]; + + if (texture is TextureBuffer textureBuffer) + { + _bufferTextureRefs[index + i] = textureBuffer; + } + else if (texture is Texture tex) + { + _textureRefs[index + i].Storage = tex; + } + else if (!_isBuffer) + { + _textureRefs[index + i].Storage = null; + } + else + { + _bufferTextureRefs[index + i] = null; + } + } + + SetDirty(); + } + + public TextureRef[] GetTextureRefs() + { + return _textureRefs; + } + + public TextureBuffer[] GetBufferTextureRefs() + { + return _bufferTextureRefs; + } + + private void SetDirty() + { + _pipeline.DirtyTextures(); + } + + public void Dispose() { } + } +} diff --git a/src/Ryujinx.Graphics.Metal/TextureBase.cs b/src/Ryujinx.Graphics.Metal/TextureBase.cs new file mode 100644 index 000000000..fecd64958 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/TextureBase.cs @@ -0,0 +1,67 @@ +using Ryujinx.Graphics.GAL; +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; +using System.Threading; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + abstract class TextureBase : IDisposable + { + private int _isValid = 1; + + public bool Valid => Volatile.Read(ref _isValid) != 0; + + protected readonly Pipeline Pipeline; + protected readonly MTLDevice Device; + protected readonly MetalRenderer Renderer; + + protected MTLTexture MtlTexture; + + public readonly TextureCreateInfo Info; + public int Width => Info.Width; + public int Height => Info.Height; + public int Depth => Info.Depth; + + public MTLPixelFormat MtlFormat { get; protected set; } + public int FirstLayer { get; protected set; } + public int FirstLevel { get; protected set; } + + public TextureBase(MTLDevice device, MetalRenderer renderer, Pipeline pipeline, TextureCreateInfo info) + { + Device = device; + Renderer = renderer; + Pipeline = pipeline; + Info = info; + } + + public MTLTexture GetHandle() + { + if (_isValid == 0) + { + return new MTLTexture(IntPtr.Zero); + } + + return MtlTexture; + } + + public virtual void Release() + { + Dispose(); + } + + public void Dispose() + { + bool wasValid = Interlocked.Exchange(ref _isValid, 0) != 0; + + if (wasValid) + { + if (MtlTexture != IntPtr.Zero) + { + MtlTexture.Dispose(); + } + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/TextureBuffer.cs b/src/Ryujinx.Graphics.Metal/TextureBuffer.cs new file mode 100644 index 000000000..483cef28b --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/TextureBuffer.cs @@ -0,0 +1,132 @@ +using Ryujinx.Common.Memory; +using Ryujinx.Graphics.GAL; +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + class TextureBuffer : TextureBase, ITexture + { + private MTLTextureDescriptor _descriptor; + private BufferHandle _bufferHandle; + private int _offset; + private int _size; + + private int _bufferCount; + private Auto _buffer; + + public TextureBuffer(MTLDevice device, MetalRenderer renderer, Pipeline pipeline, TextureCreateInfo info) : base(device, renderer, pipeline, info) + { + MTLPixelFormat pixelFormat = FormatTable.GetFormat(Info.Format); + + _descriptor = new MTLTextureDescriptor + { + PixelFormat = pixelFormat, + Usage = MTLTextureUsage.Unknown, + TextureType = MTLTextureType.TextureBuffer, + Width = (ulong)Info.Width, + Height = (ulong)Info.Height, + }; + + MtlFormat = pixelFormat; + } + + public void RebuildStorage(bool write) + { + if (MtlTexture != IntPtr.Zero) + { + MtlTexture.Dispose(); + } + + if (_buffer == null) + { + MtlTexture = default; + } + else + { + DisposableBuffer buffer = _buffer.Get(Pipeline.Cbs, _offset, _size, write); + + _descriptor.Width = (uint)(_size / Info.BytesPerPixel); + MtlTexture = buffer.Value.NewTexture(_descriptor, (ulong)_offset, (ulong)_size); + } + } + + public void CopyTo(ITexture destination, int firstLayer, int firstLevel) + { + throw new NotSupportedException(); + } + + public void CopyTo(ITexture destination, int srcLayer, int dstLayer, int srcLevel, int dstLevel) + { + throw new NotSupportedException(); + } + + public void CopyTo(ITexture destination, Extents2D srcRegion, Extents2D dstRegion, bool linearFilter) + { + throw new NotSupportedException(); + } + + public ITexture CreateView(TextureCreateInfo info, int firstLayer, int firstLevel) + { + throw new NotSupportedException(); + } + + public PinnedSpan GetData() + { + return Renderer.GetBufferData(_bufferHandle, _offset, _size); + } + + public PinnedSpan GetData(int layer, int level) + { + return GetData(); + } + + public void CopyTo(BufferRange range, int layer, int level, int stride) + { + throw new NotImplementedException(); + } + + public void SetData(MemoryOwner data) + { + Renderer.SetBufferData(_bufferHandle, _offset, data.Memory.Span); + data.Dispose(); + } + + public void SetData(MemoryOwner data, int layer, int level) + { + throw new NotSupportedException(); + } + + public void SetData(MemoryOwner data, int layer, int level, Rectangle region) + { + throw new NotSupportedException(); + } + + public void SetStorage(BufferRange buffer) + { + if (_bufferHandle == buffer.Handle && + _offset == buffer.Offset && + _size == buffer.Size && + _bufferCount == Renderer.BufferManager.BufferCount) + { + return; + } + + _bufferHandle = buffer.Handle; + _offset = buffer.Offset; + _size = buffer.Size; + _bufferCount = Renderer.BufferManager.BufferCount; + + _buffer = Renderer.BufferManager.GetBuffer(_bufferHandle, false); + } + + public override void Release() + { + _descriptor.Dispose(); + + base.Release(); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/TextureCopy.cs b/src/Ryujinx.Graphics.Metal/TextureCopy.cs new file mode 100644 index 000000000..b91a3cd89 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/TextureCopy.cs @@ -0,0 +1,265 @@ +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.GAL; +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + static class TextureCopy + { + public static ulong CopyFromOrToBuffer( + CommandBufferScoped cbs, + MTLBuffer buffer, + MTLTexture image, + TextureCreateInfo info, + bool to, + int dstLayer, + int dstLevel, + int x, + int y, + int width, + int height, + ulong offset = 0) + { + MTLBlitCommandEncoder blitCommandEncoder = cbs.Encoders.EnsureBlitEncoder(); + + bool is3D = info.Target == Target.Texture3D; + + int blockWidth = BitUtils.DivRoundUp(width, info.BlockWidth); + int blockHeight = BitUtils.DivRoundUp(height, info.BlockHeight); + ulong bytesPerRow = (ulong)BitUtils.AlignUp(blockWidth * info.BytesPerPixel, 4); + ulong bytesPerImage = bytesPerRow * (ulong)blockHeight; + + MTLOrigin origin = new MTLOrigin { x = (ulong)x, y = (ulong)y, z = is3D ? (ulong)dstLayer : 0 }; + MTLSize region = new MTLSize { width = (ulong)width, height = (ulong)height, depth = 1 }; + + uint layer = is3D ? 0 : (uint)dstLayer; + + if (to) + { + blitCommandEncoder.CopyFromTexture( + image, + layer, + (ulong)dstLevel, + origin, + region, + buffer, + offset, + bytesPerRow, + bytesPerImage); + } + else + { + blitCommandEncoder.CopyFromBuffer(buffer, offset, bytesPerRow, bytesPerImage, region, image, layer, (ulong)dstLevel, origin); + } + + return offset + bytesPerImage; + } + + public static void Copy( + CommandBufferScoped cbs, + MTLTexture srcImage, + MTLTexture dstImage, + TextureCreateInfo srcInfo, + TextureCreateInfo dstInfo, + int srcLayer, + int dstLayer, + int srcLevel, + int dstLevel) + { + int srcDepth = srcInfo.GetDepthOrLayers(); + int srcLevels = srcInfo.Levels; + + int dstDepth = dstInfo.GetDepthOrLayers(); + int dstLevels = dstInfo.Levels; + + if (dstInfo.Target == Target.Texture3D) + { + dstDepth = Math.Max(1, dstDepth >> dstLevel); + } + + int depth = Math.Min(srcDepth, dstDepth); + int levels = Math.Min(srcLevels, dstLevels); + + Copy( + cbs, + srcImage, + dstImage, + srcInfo, + dstInfo, + srcLayer, + dstLayer, + srcLevel, + dstLevel, + depth, + levels); + } + + public static void Copy( + CommandBufferScoped cbs, + MTLTexture srcImage, + MTLTexture dstImage, + TextureCreateInfo srcInfo, + TextureCreateInfo dstInfo, + int srcDepthOrLayer, + int dstDepthOrLayer, + int srcLevel, + int dstLevel, + int depthOrLayers, + int levels) + { + MTLBlitCommandEncoder blitCommandEncoder = cbs.Encoders.EnsureBlitEncoder(); + + int srcZ; + int srcLayer; + int srcDepth; + int srcLayers; + + if (srcInfo.Target == Target.Texture3D) + { + srcZ = srcDepthOrLayer; + srcLayer = 0; + srcDepth = depthOrLayers; + srcLayers = 1; + } + else + { + srcZ = 0; + srcLayer = srcDepthOrLayer; + srcDepth = 1; + srcLayers = depthOrLayers; + } + + int dstZ; + int dstLayer; + int dstLayers; + + if (dstInfo.Target == Target.Texture3D) + { + dstZ = dstDepthOrLayer; + dstLayer = 0; + dstLayers = 1; + } + else + { + dstZ = 0; + dstLayer = dstDepthOrLayer; + dstLayers = depthOrLayers; + } + + int srcWidth = srcInfo.Width; + int srcHeight = srcInfo.Height; + + int dstWidth = dstInfo.Width; + int dstHeight = dstInfo.Height; + + srcWidth = Math.Max(1, srcWidth >> srcLevel); + srcHeight = Math.Max(1, srcHeight >> srcLevel); + + dstWidth = Math.Max(1, dstWidth >> dstLevel); + dstHeight = Math.Max(1, dstHeight >> dstLevel); + + int blockWidth = 1; + int blockHeight = 1; + bool sizeInBlocks = false; + + MTLBuffer tempBuffer = default; + + if (srcInfo.Format != dstInfo.Format && (srcInfo.IsCompressed || dstInfo.IsCompressed)) + { + // Compressed alias copies need to happen through a temporary buffer. + // The data is copied from the source to the buffer, then the buffer to the destination. + // The length of the buffer should be the maximum slice size for the destination. + + tempBuffer = blitCommandEncoder.Device.NewBuffer((ulong)dstInfo.GetMipSize2D(0), MTLResourceOptions.ResourceStorageModePrivate); + } + + // When copying from a compressed to a non-compressed format, + // the non-compressed texture will have the size of the texture + // in blocks (not in texels), so we must adjust that size to + // match the size in texels of the compressed texture. + if (!srcInfo.IsCompressed && dstInfo.IsCompressed) + { + srcWidth *= dstInfo.BlockWidth; + srcHeight *= dstInfo.BlockHeight; + blockWidth = dstInfo.BlockWidth; + blockHeight = dstInfo.BlockHeight; + + sizeInBlocks = true; + } + else if (srcInfo.IsCompressed && !dstInfo.IsCompressed) + { + dstWidth *= srcInfo.BlockWidth; + dstHeight *= srcInfo.BlockHeight; + blockWidth = srcInfo.BlockWidth; + blockHeight = srcInfo.BlockHeight; + } + + int width = Math.Min(srcWidth, dstWidth); + int height = Math.Min(srcHeight, dstHeight); + + for (int level = 0; level < levels; level++) + { + // Stop copy if we are already out of the levels range. + if (level >= srcInfo.Levels || dstLevel + level >= dstInfo.Levels) + { + break; + } + + int copyWidth = sizeInBlocks ? BitUtils.DivRoundUp(width, blockWidth) : width; + int copyHeight = sizeInBlocks ? BitUtils.DivRoundUp(height, blockHeight) : height; + + int layers = Math.Max(dstLayers - dstLayer, srcLayers); + + for (int layer = 0; layer < layers; layer++) + { + if (tempBuffer.NativePtr != 0) + { + // Copy through the temp buffer + CopyFromOrToBuffer(cbs, tempBuffer, srcImage, srcInfo, true, srcLayer + layer, srcLevel + level, 0, 0, copyWidth, copyHeight); + + int dstBufferWidth = sizeInBlocks ? copyWidth * blockWidth : BitUtils.DivRoundUp(copyWidth, blockWidth); + int dstBufferHeight = sizeInBlocks ? copyHeight * blockHeight : BitUtils.DivRoundUp(copyHeight, blockHeight); + + CopyFromOrToBuffer(cbs, tempBuffer, dstImage, dstInfo, false, dstLayer + layer, dstLevel + level, 0, 0, dstBufferWidth, dstBufferHeight); + } + else if (srcInfo.Samples > 1 && srcInfo.Samples != dstInfo.Samples) + { + // TODO + + Logger.Warning?.PrintMsg(LogClass.Gpu, "Unsupported mismatching sample count copy"); + } + else + { + blitCommandEncoder.CopyFromTexture( + srcImage, + (ulong)(srcLayer + layer), + (ulong)(srcLevel + level), + new MTLOrigin { z = (ulong)srcZ }, + new MTLSize { width = (ulong)copyWidth, height = (ulong)copyHeight, depth = (ulong)srcDepth }, + dstImage, + (ulong)(dstLayer + layer), + (ulong)(dstLevel + level), + new MTLOrigin { z = (ulong)dstZ }); + } + } + + width = Math.Max(1, width >> 1); + height = Math.Max(1, height >> 1); + + if (srcInfo.Target == Target.Texture3D) + { + srcDepth = Math.Max(1, srcDepth >> 1); + } + } + + if (tempBuffer.NativePtr != 0) + { + tempBuffer.Dispose(); + } + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/VertexBufferState.cs b/src/Ryujinx.Graphics.Metal/VertexBufferState.cs new file mode 100644 index 000000000..6591fe6d6 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/VertexBufferState.cs @@ -0,0 +1,60 @@ +using Ryujinx.Graphics.GAL; +using SharpMetal.Metal; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + readonly internal struct VertexBufferState + { + public static VertexBufferState Null => new(BufferHandle.Null, 0, 0, 0); + + private readonly BufferHandle _handle; + private readonly int _offset; + private readonly int _size; + + public readonly int Stride; + public readonly int Divisor; + + public VertexBufferState(BufferHandle handle, int offset, int size, int divisor, int stride = 0) + { + _handle = handle; + _offset = offset; + _size = size; + + Stride = stride; + Divisor = divisor; + } + + public (MTLBuffer, int) GetVertexBuffer(BufferManager bufferManager, CommandBufferScoped cbs) + { + Auto autoBuffer = null; + + if (_handle != BufferHandle.Null) + { + // TODO: Handle restride if necessary + + autoBuffer = bufferManager.GetBuffer(_handle, false, out int size); + + // The original stride must be reapplied in case it was rewritten. + // TODO: Handle restride if necessary + + if (_offset >= size) + { + autoBuffer = null; + } + } + + if (autoBuffer != null) + { + int offset = _offset; + var buffer = autoBuffer.Get(cbs, offset, _size).Value; + + return (buffer, offset); + } + + return (new MTLBuffer(IntPtr.Zero), 0); + } + } +} diff --git a/src/Ryujinx.Graphics.Metal/Window.cs b/src/Ryujinx.Graphics.Metal/Window.cs new file mode 100644 index 000000000..65a47d217 --- /dev/null +++ b/src/Ryujinx.Graphics.Metal/Window.cs @@ -0,0 +1,231 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.GAL; +using Ryujinx.Graphics.Metal.Effects; +using SharpMetal.ObjectiveCCore; +using SharpMetal.QuartzCore; +using System; +using System.Runtime.Versioning; + +namespace Ryujinx.Graphics.Metal +{ + [SupportedOSPlatform("macos")] + class Window : IWindow, IDisposable + { + public bool ScreenCaptureRequested { get; set; } + + private readonly MetalRenderer _renderer; + private readonly CAMetalLayer _metalLayer; + + private int _width; + private int _height; + + private int _requestedWidth; + private int _requestedHeight; + + // private bool _vsyncEnabled; + private AntiAliasing _currentAntiAliasing; + private bool _updateEffect; + private IPostProcessingEffect _effect; + private IScalingFilter _scalingFilter; + private bool _isLinear; + // private float _scalingFilterLevel; + private bool _updateScalingFilter; + private ScalingFilter _currentScalingFilter; + // private bool _colorSpacePassthroughEnabled; + + public Window(MetalRenderer renderer, CAMetalLayer metalLayer) + { + _renderer = renderer; + _metalLayer = metalLayer; + } + + private unsafe void ResizeIfNeeded() + { + if (_requestedWidth != 0 && _requestedHeight != 0) + { + // TODO: This is actually a CGSize, but there is no overload for that, so fill the first two fields of rect with the size. + var rect = new NSRect(_requestedWidth, _requestedHeight, 0, 0); + + ObjectiveC.objc_msgSend(_metalLayer, "setDrawableSize:", rect); + + _requestedWidth = 0; + _requestedHeight = 0; + } + } + + public unsafe void Present(ITexture texture, ImageCrop crop, Action swapBuffersCallback) + { + if (_renderer.Pipeline is Pipeline pipeline && texture is Texture tex) + { + ResizeIfNeeded(); + + var drawable = new CAMetalDrawable(ObjectiveC.IntPtr_objc_msgSend(_metalLayer, "nextDrawable")); + + _width = (int)drawable.Texture.Width; + _height = (int)drawable.Texture.Height; + + UpdateEffect(); + + if (_effect != null) + { + // TODO: Run Effects + // view = _effect.Run() + } + + int srcX0, srcX1, srcY0, srcY1; + + if (crop.Left == 0 && crop.Right == 0) + { + srcX0 = 0; + srcX1 = tex.Width; + } + else + { + srcX0 = crop.Left; + srcX1 = crop.Right; + } + + if (crop.Top == 0 && crop.Bottom == 0) + { + srcY0 = 0; + srcY1 = tex.Height; + } + else + { + srcY0 = crop.Top; + srcY1 = crop.Bottom; + } + + if (ScreenCaptureRequested) + { + // TODO: Support screen captures + + ScreenCaptureRequested = false; + } + + float ratioX = crop.IsStretched ? 1.0f : MathF.Min(1.0f, _height * crop.AspectRatioX / (_width * crop.AspectRatioY)); + float ratioY = crop.IsStretched ? 1.0f : MathF.Min(1.0f, _width * crop.AspectRatioY / (_height * crop.AspectRatioX)); + + int dstWidth = (int)(_width * ratioX); + int dstHeight = (int)(_height * ratioY); + + int dstPaddingX = (_width - dstWidth) / 2; + int dstPaddingY = (_height - dstHeight) / 2; + + int dstX0 = crop.FlipX ? _width - dstPaddingX : dstPaddingX; + int dstX1 = crop.FlipX ? dstPaddingX : _width - dstPaddingX; + + int dstY0 = crop.FlipY ? _height - dstPaddingY : dstPaddingY; + int dstY1 = crop.FlipY ? dstPaddingY : _height - dstPaddingY; + + if (_scalingFilter != null) + { + // TODO: Run scaling filter + } + + pipeline.Present( + drawable, + tex, + new Extents2D(srcX0, srcY0, srcX1, srcY1), + new Extents2D(dstX0, dstY0, dstX1, dstY1), + _isLinear); + } + } + + public void SetSize(int width, int height) + { + _requestedWidth = width; + _requestedHeight = height; + } + + public void ChangeVSyncMode(VSyncMode vSyncMode) + { + //_vSyncMode = vSyncMode; + } + + public void SetAntiAliasing(AntiAliasing effect) + { + if (_currentAntiAliasing == effect && _effect != null) + { + return; + } + + _currentAntiAliasing = effect; + + _updateEffect = true; + } + + public void SetScalingFilter(ScalingFilter type) + { + if (_currentScalingFilter == type && _effect != null) + { + return; + } + + _currentScalingFilter = type; + + _updateScalingFilter = true; + } + + public void SetScalingFilterLevel(float level) + { + // _scalingFilterLevel = level; + _updateScalingFilter = true; + } + + public void SetColorSpacePassthrough(bool colorSpacePassThroughEnabled) + { + // _colorSpacePassthroughEnabled = colorSpacePassThroughEnabled; + } + + private void UpdateEffect() + { + if (_updateEffect) + { + _updateEffect = false; + + switch (_currentAntiAliasing) + { + case AntiAliasing.Fxaa: + _effect?.Dispose(); + Logger.Warning?.PrintMsg(LogClass.Gpu, "FXAA not implemented for Metal backend!"); + break; + case AntiAliasing.None: + _effect?.Dispose(); + _effect = null; + break; + case AntiAliasing.SmaaLow: + case AntiAliasing.SmaaMedium: + case AntiAliasing.SmaaHigh: + case AntiAliasing.SmaaUltra: + // var quality = _currentAntiAliasing - AntiAliasing.SmaaLow; + Logger.Warning?.PrintMsg(LogClass.Gpu, "SMAA not implemented for Metal backend!"); + break; + } + } + + if (_updateScalingFilter) + { + _updateScalingFilter = false; + + switch (_currentScalingFilter) + { + case ScalingFilter.Bilinear: + case ScalingFilter.Nearest: + _scalingFilter?.Dispose(); + _scalingFilter = null; + _isLinear = _currentScalingFilter == ScalingFilter.Bilinear; + break; + case ScalingFilter.Fsr: + Logger.Warning?.PrintMsg(LogClass.Gpu, "FSR not implemented for Metal backend!"); + break; + } + } + } + + public void Dispose() + { + _metalLayer.Dispose(); + } + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/CodeGenContext.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/CodeGenContext.cs new file mode 100644 index 000000000..2fa188ea9 --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/CodeGenContext.cs @@ -0,0 +1,108 @@ +using Ryujinx.Graphics.Shader.StructuredIr; +using Ryujinx.Graphics.Shader.Translation; +using System.Text; + +namespace Ryujinx.Graphics.Shader.CodeGen.Msl +{ + class CodeGenContext + { + public const string Tab = " "; + + // The number of additional arguments that every function (except for the main one) must have (for instance support_buffer) + public const int AdditionalArgCount = 2; + + public StructuredFunction CurrentFunction { get; set; } + + public StructuredProgramInfo Info { get; } + + public AttributeUsage AttributeUsage { get; } + public ShaderDefinitions Definitions { get; } + public ShaderProperties Properties { get; } + public HostCapabilities HostCapabilities { get; } + public ILogger Logger { get; } + public TargetApi TargetApi { get; } + + public OperandManager OperandManager { get; } + + private readonly StringBuilder _sb; + + private int _level; + + private string _indentation; + + public CodeGenContext(StructuredProgramInfo info, CodeGenParameters parameters) + { + Info = info; + AttributeUsage = parameters.AttributeUsage; + Definitions = parameters.Definitions; + Properties = parameters.Properties; + HostCapabilities = parameters.HostCapabilities; + Logger = parameters.Logger; + TargetApi = parameters.TargetApi; + + OperandManager = new OperandManager(); + + _sb = new StringBuilder(); + } + + public void AppendLine() + { + _sb.AppendLine(); + } + + public void AppendLine(string str) + { + _sb.AppendLine(_indentation + str); + } + + public string GetCode() + { + return _sb.ToString(); + } + + public void EnterScope(string prefix = "") + { + AppendLine(prefix + "{"); + + _level++; + + UpdateIndentation(); + } + + public void LeaveScope(string suffix = "") + { + if (_level == 0) + { + return; + } + + _level--; + + UpdateIndentation(); + + AppendLine("}" + suffix); + } + + public StructuredFunction GetFunction(int id) + { + return Info.Functions[id]; + } + + private void UpdateIndentation() + { + _indentation = GetIndentation(_level); + } + + private static string GetIndentation(int level) + { + string indentation = string.Empty; + + for (int index = 0; index < level; index++) + { + indentation += Tab; + } + + return indentation; + } + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Declarations.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Declarations.cs new file mode 100644 index 000000000..912b162d2 --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Declarations.cs @@ -0,0 +1,578 @@ +using Ryujinx.Common; +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.Shader.IntermediateRepresentation; +using Ryujinx.Graphics.Shader.StructuredIr; +using Ryujinx.Graphics.Shader.Translation; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Ryujinx.Graphics.Shader.CodeGen.Msl +{ + static class Declarations + { + /* + * Description of MSL Binding Model + * + * There are a few fundamental differences between how GLSL and MSL handle I/O. + * This comment will set out to describe the reasons why things are done certain ways + * and to describe the overall binding model that we're striving for here. + * + * Main I/O Structs + * + * Each stage has a main input and output struct (if applicable) labeled as [Stage][In/Out], i.e VertexIn. + * Every field within these structs is labeled with an [[attribute(n)]] property, + * and the overall struct is labeled with [[stage_in]] for input structs, and defined as the + * output type of the main shader function for the output struct. This struct also contains special + * attribute-based properties like [[position]] that would be "built-ins" in a GLSL context. + * + * These structs are passed as inputs to all inline functions due to containing "built-ins" + * that inline functions assume access to. + * + * Vertex & Zero Buffers + * + * Binding indices 0-16 are reserved for vertex buffers, and binding 18 is reserved for the zero buffer. + * + * Uniforms & Storage Buffers + * + * Uniforms and storage buffers are tightly packed into their respective argument buffers + * (effectively ignoring binding indices at shader level), with each pointer to the corresponding + * struct that defines the layout and fields of these buffers (usually just a single data array), laid + * out one after the other in ascending order of their binding index. + * + * The uniforms argument buffer is always bound at a fixed index of 20. + * The storage buffers argument buffer is always bound at a fixed index of 21. + * + * These structs are passed as inputs to all inline functions as in GLSL or SPIRV, + * uniforms and storage buffers would be globals, and inline functions assume access to these buffers. + * + * Samplers & Textures + * + * Metal does not have a combined image sampler like sampler2D in GLSL, as a result we need to bind + * an individual texture and a sampler object for each instance of a combined image sampler. + * Samplers and textures are bound in a shared argument buffer. This argument buffer is tightly packed + * (effectively ignoring binding indices at shader level), with texture and their samplers (if present) + * laid out one after the other in ascending order of their binding index. + * + * The samplers and textures argument buffer is always bound at a fixed index of 22. + * + */ + + public static int[] Declare(CodeGenContext context, StructuredProgramInfo info) + { + // TODO: Re-enable this warning + context.AppendLine("#pragma clang diagnostic ignored \"-Wunused-variable\""); + context.AppendLine(); + context.AppendLine("#include "); + context.AppendLine("#include "); + context.AppendLine(); + context.AppendLine("using namespace metal;"); + context.AppendLine(); + + var fsi = (info.HelperFunctionsMask & HelperFunctionsMask.FSI) != 0; + + DeclareInputAttributes(context, info.IoDefinitions.Where(x => IsUserDefined(x, StorageKind.Input))); + context.AppendLine(); + DeclareOutputAttributes(context, info.IoDefinitions.Where(x => x.StorageKind == StorageKind.Output)); + context.AppendLine(); + DeclareBufferStructures(context, context.Properties.ConstantBuffers.Values.OrderBy(x => x.Binding).ToArray(), true, fsi); + DeclareBufferStructures(context, context.Properties.StorageBuffers.Values.OrderBy(x => x.Binding).ToArray(), false, fsi); + + // We need to declare each set as a new struct + var textureDefinitions = context.Properties.Textures.Values + .GroupBy(x => x.Set) + .ToDictionary(x => x.Key, x => x.OrderBy(y => y.Binding).ToArray()); + + var imageDefinitions = context.Properties.Images.Values + .GroupBy(x => x.Set) + .ToDictionary(x => x.Key, x => x.OrderBy(y => y.Binding).ToArray()); + + var textureSets = textureDefinitions.Keys.ToArray(); + var imageSets = imageDefinitions.Keys.ToArray(); + + var sets = textureSets.Union(imageSets).ToArray(); + + foreach (var set in textureDefinitions) + { + DeclareTextures(context, set.Value, set.Key); + } + + foreach (var set in imageDefinitions) + { + DeclareImages(context, set.Value, set.Key, fsi); + } + + if ((info.HelperFunctionsMask & HelperFunctionsMask.FindLSB) != 0) + { + AppendHelperFunction(context, "Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/FindLSB.metal"); + } + + if ((info.HelperFunctionsMask & HelperFunctionsMask.FindMSBS32) != 0) + { + AppendHelperFunction(context, "Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/FindMSBS32.metal"); + } + + if ((info.HelperFunctionsMask & HelperFunctionsMask.FindMSBU32) != 0) + { + AppendHelperFunction(context, "Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/FindMSBU32.metal"); + } + + if ((info.HelperFunctionsMask & HelperFunctionsMask.SwizzleAdd) != 0) + { + AppendHelperFunction(context, "Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/SwizzleAdd.metal"); + } + + if ((info.HelperFunctionsMask & HelperFunctionsMask.Precise) != 0) + { + AppendHelperFunction(context, "Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/Precise.metal"); + } + + return sets; + } + + static bool IsUserDefined(IoDefinition ioDefinition, StorageKind storageKind) + { + return ioDefinition.StorageKind == storageKind && ioDefinition.IoVariable == IoVariable.UserDefined; + } + + public static void DeclareLocals(CodeGenContext context, StructuredFunction function, ShaderStage stage, bool isMainFunc = false) + { + if (isMainFunc) + { + // TODO: Support OaIndexing + if (context.Definitions.IaIndexing) + { + context.EnterScope($"array {Defaults.IAttributePrefix} = "); + + for (int i = 0; i < Constants.MaxAttributes; i++) + { + context.AppendLine($"in.{Defaults.IAttributePrefix}{i},"); + } + + context.LeaveScope(";"); + } + + DeclareMemories(context, context.Properties.LocalMemories.Values, isShared: false); + DeclareMemories(context, context.Properties.SharedMemories.Values, isShared: true); + + switch (stage) + { + case ShaderStage.Vertex: + context.AppendLine("VertexOut out = {};"); + // TODO: Only add if necessary + context.AppendLine("uint instance_index = instance_id + base_instance;"); + break; + case ShaderStage.Fragment: + context.AppendLine("FragmentOut out = {};"); + break; + } + + // TODO: Only add if necessary + if (stage != ShaderStage.Compute) + { + // MSL does not give us access to [[thread_index_in_simdgroup]] + // outside compute. But we may still need to provide this value in frag/vert. + context.AppendLine("uint thread_index_in_simdgroup = simd_prefix_exclusive_sum(1);"); + } + } + + foreach (AstOperand decl in function.Locals) + { + string name = context.OperandManager.DeclareLocal(decl); + + context.AppendLine(GetVarTypeName(decl.VarType) + " " + name + ";"); + } + } + + public static string GetVarTypeName(AggregateType type, bool atomic = false) + { + var s32 = atomic ? "atomic_int" : "int"; + var u32 = atomic ? "atomic_uint" : "uint"; + + return type switch + { + AggregateType.Void => "void", + AggregateType.Bool => "bool", + AggregateType.FP32 => "float", + AggregateType.S32 => s32, + AggregateType.U32 => u32, + AggregateType.Vector2 | AggregateType.Bool => "bool2", + AggregateType.Vector2 | AggregateType.FP32 => "float2", + AggregateType.Vector2 | AggregateType.S32 => "int2", + AggregateType.Vector2 | AggregateType.U32 => "uint2", + AggregateType.Vector3 | AggregateType.Bool => "bool3", + AggregateType.Vector3 | AggregateType.FP32 => "float3", + AggregateType.Vector3 | AggregateType.S32 => "int3", + AggregateType.Vector3 | AggregateType.U32 => "uint3", + AggregateType.Vector4 | AggregateType.Bool => "bool4", + AggregateType.Vector4 | AggregateType.FP32 => "float4", + AggregateType.Vector4 | AggregateType.S32 => "int4", + AggregateType.Vector4 | AggregateType.U32 => "uint4", + _ => throw new ArgumentException($"Invalid variable type \"{type}\"."), + }; + } + + private static void DeclareMemories(CodeGenContext context, IEnumerable memories, bool isShared) + { + string prefix = isShared ? "threadgroup " : string.Empty; + + foreach (var memory in memories) + { + string arraySize = ""; + if ((memory.Type & AggregateType.Array) != 0) + { + arraySize = $"[{memory.ArrayLength}]"; + } + var typeName = GetVarTypeName(memory.Type & ~AggregateType.Array); + context.AppendLine($"{prefix}{typeName} {memory.Name}{arraySize};"); + } + } + + private static void DeclareBufferStructures(CodeGenContext context, BufferDefinition[] buffers, bool constant, bool fsi) + { + var name = constant ? "ConstantBuffers" : "StorageBuffers"; + var addressSpace = constant ? "constant" : "device"; + + string[] bufferDec = new string[buffers.Length]; + + for (int i = 0; i < buffers.Length; i++) + { + BufferDefinition buffer = buffers[i]; + + var needsPadding = buffer.Layout == BufferLayout.Std140; + string fsiSuffix = !constant && fsi ? " [[raster_order_group(0)]]" : ""; + + bufferDec[i] = $"{addressSpace} {Defaults.StructPrefix}_{buffer.Name}* {buffer.Name}{fsiSuffix};"; + + context.AppendLine($"struct {Defaults.StructPrefix}_{buffer.Name}"); + context.EnterScope(); + + foreach (StructureField field in buffer.Type.Fields) + { + var type = field.Type; + type |= (needsPadding && (field.Type & AggregateType.Array) != 0) + ? AggregateType.Vector4 + : AggregateType.Invalid; + + type &= ~AggregateType.Array; + + string typeName = GetVarTypeName(type); + string arraySuffix = ""; + + if (field.Type.HasFlag(AggregateType.Array)) + { + if (field.ArrayLength > 0) + { + arraySuffix = $"[{field.ArrayLength}]"; + } + else + { + // Probably UB, but this is the approach that MVK takes + arraySuffix = "[1]"; + } + } + + context.AppendLine($"{typeName} {field.Name}{arraySuffix};"); + } + + context.LeaveScope(";"); + context.AppendLine(); + } + + context.AppendLine($"struct {name}"); + context.EnterScope(); + + foreach (var declaration in bufferDec) + { + context.AppendLine(declaration); + } + + context.LeaveScope(";"); + context.AppendLine(); + } + + private static void DeclareTextures(CodeGenContext context, TextureDefinition[] textures, int set) + { + var setName = GetNameForSet(set); + context.AppendLine($"struct {setName}"); + context.EnterScope(); + + List textureDec = []; + + foreach (TextureDefinition texture in textures) + { + if (texture.Type != SamplerType.None) + { + var textureTypeName = texture.Type.ToMslTextureType(texture.Format.GetComponentType()); + + if (texture.ArrayLength > 1) + { + textureTypeName = $"array<{textureTypeName}, {texture.ArrayLength}>"; + } + + textureDec.Add($"{textureTypeName} tex_{texture.Name};"); + } + + if (!texture.Separate && texture.Type != SamplerType.TextureBuffer) + { + var samplerType = "sampler"; + + if (texture.ArrayLength > 1) + { + samplerType = $"array<{samplerType}, {texture.ArrayLength}>"; + } + + textureDec.Add($"{samplerType} samp_{texture.Name};"); + } + } + + foreach (var declaration in textureDec) + { + context.AppendLine(declaration); + } + + context.LeaveScope(";"); + context.AppendLine(); + } + + private static void DeclareImages(CodeGenContext context, TextureDefinition[] images, int set, bool fsi) + { + var setName = GetNameForSet(set); + context.AppendLine($"struct {setName}"); + context.EnterScope(); + + string[] imageDec = new string[images.Length]; + + for (int i = 0; i < images.Length; i++) + { + TextureDefinition image = images[i]; + + var imageTypeName = image.Type.ToMslTextureType(image.Format.GetComponentType(), true); + if (image.ArrayLength > 1) + { + imageTypeName = $"array<{imageTypeName}, {image.ArrayLength}>"; + } + + string fsiSuffix = fsi ? " [[raster_order_group(0)]]" : ""; + + imageDec[i] = $"{imageTypeName} {image.Name}{fsiSuffix};"; + } + + foreach (var declaration in imageDec) + { + context.AppendLine(declaration); + } + + context.LeaveScope(";"); + context.AppendLine(); + } + + private static void DeclareInputAttributes(CodeGenContext context, IEnumerable inputs) + { + if (context.Definitions.Stage == ShaderStage.Compute) + { + return; + } + + switch (context.Definitions.Stage) + { + case ShaderStage.Vertex: + context.AppendLine("struct VertexIn"); + break; + case ShaderStage.Fragment: + context.AppendLine("struct FragmentIn"); + break; + } + + context.EnterScope(); + + if (context.Definitions.Stage == ShaderStage.Fragment) + { + // TODO: check if it's needed + context.AppendLine("float4 position [[position, invariant]];"); + context.AppendLine("bool front_facing [[front_facing]];"); + context.AppendLine("float2 point_coord [[point_coord]];"); + context.AppendLine("uint primitive_id [[primitive_id]];"); + } + + if (context.Definitions.IaIndexing) + { + // MSL does not support arrays in stage I/O + // We need to use the SPIRV-Cross workaround + for (int i = 0; i < Constants.MaxAttributes; i++) + { + var suffix = context.Definitions.Stage == ShaderStage.Fragment ? $"[[user(loc{i})]]" : $"[[attribute({i})]]"; + context.AppendLine($"float4 {Defaults.IAttributePrefix}{i} {suffix};"); + } + } + + if (inputs.Any()) + { + foreach (var ioDefinition in inputs.OrderBy(x => x.Location)) + { + if (context.Definitions.IaIndexing && ioDefinition.IoVariable == IoVariable.UserDefined) + { + continue; + } + + string iq = string.Empty; + + if (context.Definitions.Stage == ShaderStage.Fragment) + { + iq = context.Definitions.ImapTypes[ioDefinition.Location].GetFirstUsedType() switch + { + PixelImap.Constant => "[[flat]] ", + PixelImap.ScreenLinear => "[[center_no_perspective]] ", + _ => string.Empty, + }; + } + + string type = ioDefinition.IoVariable switch + { + // IoVariable.Position => "float4", + IoVariable.GlobalId => "uint3", + IoVariable.VertexId => "uint", + IoVariable.VertexIndex => "uint", + // IoVariable.PointCoord => "float2", + _ => GetVarTypeName(context.Definitions.GetUserDefinedType(ioDefinition.Location, isOutput: false)) + }; + string name = ioDefinition.IoVariable switch + { + // IoVariable.Position => "position", + IoVariable.GlobalId => "global_id", + IoVariable.VertexId => "vertex_id", + IoVariable.VertexIndex => "vertex_index", + // IoVariable.PointCoord => "point_coord", + _ => $"{Defaults.IAttributePrefix}{ioDefinition.Location}" + }; + string suffix = ioDefinition.IoVariable switch + { + // IoVariable.Position => "[[position, invariant]]", + IoVariable.GlobalId => "[[thread_position_in_grid]]", + IoVariable.VertexId => "[[vertex_id]]", + // TODO: Avoid potential redeclaration + IoVariable.VertexIndex => "[[vertex_id]]", + // IoVariable.PointCoord => "[[point_coord]]", + IoVariable.UserDefined => context.Definitions.Stage == ShaderStage.Fragment ? $"[[user(loc{ioDefinition.Location})]]" : $"[[attribute({ioDefinition.Location})]]", + _ => "" + }; + + context.AppendLine($"{type} {name} {iq}{suffix};"); + } + } + + context.LeaveScope(";"); + } + + private static void DeclareOutputAttributes(CodeGenContext context, IEnumerable outputs) + { + switch (context.Definitions.Stage) + { + case ShaderStage.Vertex: + context.AppendLine("struct VertexOut"); + break; + case ShaderStage.Fragment: + context.AppendLine("struct FragmentOut"); + break; + case ShaderStage.Compute: + context.AppendLine("struct KernelOut"); + break; + } + + context.EnterScope(); + + if (context.Definitions.OaIndexing) + { + // MSL does not support arrays in stage I/O + // We need to use the SPIRV-Cross workaround + for (int i = 0; i < Constants.MaxAttributes; i++) + { + context.AppendLine($"float4 {Defaults.OAttributePrefix}{i} [[user(loc{i})]];"); + } + } + + if (outputs.Any()) + { + outputs = outputs.OrderBy(x => x.Location); + + if (context.Definitions.Stage == ShaderStage.Fragment && context.Definitions.DualSourceBlend) + { + IoDefinition firstOutput = outputs.ElementAtOrDefault(0); + IoDefinition secondOutput = outputs.ElementAtOrDefault(1); + + var type1 = GetVarTypeName(context.Definitions.GetFragmentOutputColorType(firstOutput.Location)); + var type2 = GetVarTypeName(context.Definitions.GetFragmentOutputColorType(secondOutput.Location)); + + var name1 = $"color{firstOutput.Location}"; + var name2 = $"color{firstOutput.Location + 1}"; + + context.AppendLine($"{type1} {name1} [[color({firstOutput.Location}), index(0)]];"); + context.AppendLine($"{type2} {name2} [[color({firstOutput.Location}), index(1)]];"); + + outputs = outputs.Skip(2); + } + + foreach (var ioDefinition in outputs) + { + if (context.Definitions.OaIndexing && ioDefinition.IoVariable == IoVariable.UserDefined) + { + continue; + } + + string type = ioDefinition.IoVariable switch + { + IoVariable.Position => "float4", + IoVariable.PointSize => "float", + IoVariable.FragmentOutputColor => GetVarTypeName(context.Definitions.GetFragmentOutputColorType(ioDefinition.Location)), + IoVariable.FragmentOutputDepth => "float", + IoVariable.ClipDistance => "float", + _ => GetVarTypeName(context.Definitions.GetUserDefinedType(ioDefinition.Location, isOutput: true)) + }; + string name = ioDefinition.IoVariable switch + { + IoVariable.Position => "position", + IoVariable.PointSize => "point_size", + IoVariable.FragmentOutputColor => $"color{ioDefinition.Location}", + IoVariable.FragmentOutputDepth => "depth", + IoVariable.ClipDistance => "clip_distance", + _ => $"{Defaults.OAttributePrefix}{ioDefinition.Location}" + }; + string suffix = ioDefinition.IoVariable switch + { + IoVariable.Position => "[[position, invariant]]", + IoVariable.PointSize => "[[point_size]]", + IoVariable.UserDefined => $"[[user(loc{ioDefinition.Location})]]", + IoVariable.FragmentOutputColor => $"[[color({ioDefinition.Location})]]", + IoVariable.FragmentOutputDepth => "[[depth(any)]]", + IoVariable.ClipDistance => $"[[clip_distance]][{Defaults.TotalClipDistances}]", + _ => "" + }; + + context.AppendLine($"{type} {name} {suffix};"); + } + } + + context.LeaveScope(";"); + } + + private static void AppendHelperFunction(CodeGenContext context, string filename) + { + string code = EmbeddedResources.ReadAllText(filename); + + code = code.Replace("\t", CodeGenContext.Tab); + + context.AppendLine(code); + context.AppendLine(); + } + + public static string GetNameForSet(int set, bool forVar = false) + { + return (uint)set switch + { + Defaults.TexturesSetIndex => forVar ? "textures" : "Textures", + Defaults.ImagesSetIndex => forVar ? "images" : "Images", + _ => $"{(forVar ? "set" : "Set")}{set}" + }; + } + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Defaults.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Defaults.cs new file mode 100644 index 000000000..511a2f606 --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Defaults.cs @@ -0,0 +1,34 @@ +namespace Ryujinx.Graphics.Shader.CodeGen.Msl +{ + static class Defaults + { + public const string LocalNamePrefix = "temp"; + + public const string PerPatchAttributePrefix = "patchAttr"; + public const string IAttributePrefix = "inAttr"; + public const string OAttributePrefix = "outAttr"; + + public const string StructPrefix = "struct"; + + public const string ArgumentNamePrefix = "a"; + + public const string UndefinedName = "0"; + + public const int MaxVertexBuffers = 16; + + public const uint ZeroBufferIndex = MaxVertexBuffers; + public const uint BaseSetIndex = MaxVertexBuffers + 1; + + public const uint ConstantBuffersIndex = BaseSetIndex; + public const uint StorageBuffersIndex = BaseSetIndex + 1; + public const uint TexturesIndex = BaseSetIndex + 2; + public const uint ImagesIndex = BaseSetIndex + 3; + + public const uint ConstantBuffersSetIndex = 0; + public const uint StorageBuffersSetIndex = 1; + public const uint TexturesSetIndex = 2; + public const uint ImagesSetIndex = 3; + + public const int TotalClipDistances = 8; + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/FindLSB.metal b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/FindLSB.metal new file mode 100644 index 000000000..ad786adb3 --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/FindLSB.metal @@ -0,0 +1,5 @@ +template +inline T findLSB(T x) +{ + return select(ctz(x), T(-1), x == T(0)); +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/FindMSBS32.metal b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/FindMSBS32.metal new file mode 100644 index 000000000..af4eb6cbd --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/FindMSBS32.metal @@ -0,0 +1,5 @@ +template +inline T findMSBS32(T x) +{ + return select(clz(T(0)) - (clz(x) + T(1)), T(-1), x == T(0)); +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/FindMSBU32.metal b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/FindMSBU32.metal new file mode 100644 index 000000000..6d97c41a9 --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/FindMSBU32.metal @@ -0,0 +1,6 @@ +template +inline T findMSBU32(T x) +{ + T v = select(x, T(-1) - x, x < T(0)); + return select(clz(T(0)) - (clz(v) + T(1)), T(-1), v == T(0)); +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/HelperFunctionNames.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/HelperFunctionNames.cs new file mode 100644 index 000000000..370159a0e --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/HelperFunctionNames.cs @@ -0,0 +1,10 @@ +namespace Ryujinx.Graphics.Shader.CodeGen.Msl +{ + static class HelperFunctionNames + { + public static string FindLSB = "findLSB"; + public static string FindMSBS32 = "findMSBS32"; + public static string FindMSBU32 = "findMSBU32"; + public static string SwizzleAdd = "swizzleAdd"; + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/Precise.metal b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/Precise.metal new file mode 100644 index 000000000..366bea1ac --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/Precise.metal @@ -0,0 +1,14 @@ +template +[[clang::optnone]] T PreciseFAdd(T l, T r) { + return fma(T(1), l, r); +} + +template +[[clang::optnone]] T PreciseFSub(T l, T r) { + return fma(T(-1), r, l); +} + +template +[[clang::optnone]] T PreciseFMul(T l, T r) { + return fma(l, r, T(0)); +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/SwizzleAdd.metal b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/SwizzleAdd.metal new file mode 100644 index 000000000..22a079b01 --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/HelperFunctions/SwizzleAdd.metal @@ -0,0 +1,7 @@ +float swizzleAdd(float x, float y, int mask, uint thread_index_in_simdgroup) +{ + float4 xLut = float4(1.0, -1.0, 1.0, 0.0); + float4 yLut = float4(1.0, 1.0, -1.0, 1.0); + int lutIdx = (mask >> (int(thread_index_in_simdgroup & 3u) * 2)) & 3; + return x * xLut[lutIdx] + y * yLut[lutIdx]; +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGen.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGen.cs new file mode 100644 index 000000000..715688987 --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGen.cs @@ -0,0 +1,185 @@ +using Ryujinx.Graphics.Shader.IntermediateRepresentation; +using Ryujinx.Graphics.Shader.StructuredIr; +using Ryujinx.Graphics.Shader.Translation; +using System; +using System.Text; +using static Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions.InstGenBallot; +using static Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions.InstGenBarrier; +using static Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions.InstGenCall; +using static Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions.InstGenHelper; +using static Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions.InstGenMemory; +using static Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions.InstGenVector; +using static Ryujinx.Graphics.Shader.StructuredIr.InstructionInfo; + +namespace Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions +{ + static class InstGen + { + public static string GetExpression(CodeGenContext context, IAstNode node) + { + if (node is AstOperation operation) + { + return GetExpression(context, operation); + } + else if (node is AstOperand operand) + { + return context.OperandManager.GetExpression(context, operand); + } + + throw new ArgumentException($"Invalid node type \"{node?.GetType().Name ?? "null"}\"."); + } + + private static string GetExpression(CodeGenContext context, AstOperation operation) + { + Instruction inst = operation.Inst; + + InstInfo info = GetInstructionInfo(inst); + + if ((info.Type & InstType.Call) != 0) + { + bool atomic = (info.Type & InstType.Atomic) != 0; + + int arity = (int)(info.Type & InstType.ArityMask); + + StringBuilder builder = new(); + + if (atomic && (operation.StorageKind == StorageKind.StorageBuffer || operation.StorageKind == StorageKind.SharedMemory)) + { + AggregateType dstType = operation.Inst == Instruction.AtomicMaxS32 || operation.Inst == Instruction.AtomicMinS32 + ? AggregateType.S32 + : AggregateType.U32; + + var shared = operation.StorageKind == StorageKind.SharedMemory; + + builder.Append($"({(shared ? "threadgroup" : "device")} {Declarations.GetVarTypeName(dstType, true)}*)&{GenerateLoadOrStore(context, operation, isStore: false)}"); + + for (int argIndex = operation.SourcesCount - arity + 2; argIndex < operation.SourcesCount; argIndex++) + { + builder.Append($", {GetSourceExpr(context, operation.GetSource(argIndex), dstType)}, memory_order_relaxed"); + } + } + else + { + for (int argIndex = 0; argIndex < arity; argIndex++) + { + if (argIndex != 0) + { + builder.Append(", "); + } + + AggregateType dstType = GetSrcVarType(inst, argIndex); + + builder.Append(GetSourceExpr(context, operation.GetSource(argIndex), dstType)); + } + + if ((operation.Inst & Instruction.Mask) == Instruction.SwizzleAdd) + { + // SwizzleAdd takes one last argument, the thread_index_in_simdgroup + builder.Append(", thread_index_in_simdgroup"); + } + } + + return $"{info.OpName}({builder})"; + } + else if ((info.Type & InstType.Op) != 0) + { + string op = info.OpName; + + if (inst == Instruction.Return && operation.SourcesCount != 0) + { + return $"{op} {GetSourceExpr(context, operation.GetSource(0), context.CurrentFunction.ReturnType)}"; + } + if (inst == Instruction.Return && context.Definitions.Stage is ShaderStage.Vertex or ShaderStage.Fragment) + { + return $"{op} out"; + } + + int arity = (int)(info.Type & InstType.ArityMask); + + string[] expr = new string[arity]; + + for (int index = 0; index < arity; index++) + { + IAstNode src = operation.GetSource(index); + + string srcExpr = GetSourceExpr(context, src, GetSrcVarType(inst, index)); + + bool isLhs = arity == 2 && index == 0; + + expr[index] = Enclose(srcExpr, src, inst, info, isLhs); + } + + switch (arity) + { + case 0: + return op; + + case 1: + return op + expr[0]; + + case 2: + if (operation.ForcePrecise) + { + var func = (inst & Instruction.Mask) switch + { + Instruction.Add => "PreciseFAdd", + Instruction.Subtract => "PreciseFSub", + Instruction.Multiply => "PreciseFMul", + }; + + return $"{func}({expr[0]}, {expr[1]})"; + } + + return $"{expr[0]} {op} {expr[1]}"; + + case 3: + return $"{expr[0]} {op[0]} {expr[1]} {op[1]} {expr[2]}"; + } + } + else if ((info.Type & InstType.Special) != 0) + { + switch (inst & Instruction.Mask) + { + case Instruction.Ballot: + return Ballot(context, operation); + case Instruction.Call: + return Call(context, operation); + case Instruction.FSIBegin: + case Instruction.FSIEnd: + return "// FSI implemented with raster order groups in MSL"; + case Instruction.GroupMemoryBarrier: + case Instruction.MemoryBarrier: + case Instruction.Barrier: + return Barrier(context, operation); + case Instruction.ImageLoad: + case Instruction.ImageStore: + case Instruction.ImageAtomic: + return ImageLoadOrStore(context, operation); + case Instruction.Load: + return Load(context, operation); + case Instruction.Lod: + return Lod(context, operation); + case Instruction.Store: + return Store(context, operation); + case Instruction.TextureSample: + return TextureSample(context, operation); + case Instruction.TextureQuerySamples: + return TextureQuerySamples(context, operation); + case Instruction.TextureQuerySize: + return TextureQuerySize(context, operation); + case Instruction.PackHalf2x16: + return PackHalf2x16(context, operation); + case Instruction.UnpackHalf2x16: + return UnpackHalf2x16(context, operation); + case Instruction.VectorExtract: + return VectorExtract(context, operation); + case Instruction.VoteAllEqual: + return VoteAllEqual(context, operation); + } + } + + // TODO: Return this to being an error + return $"Unexpected instruction type \"{info.Type}\"."; + } + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenBallot.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenBallot.cs new file mode 100644 index 000000000..19a065d77 --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenBallot.cs @@ -0,0 +1,30 @@ +using Ryujinx.Graphics.Shader.StructuredIr; +using Ryujinx.Graphics.Shader.Translation; + +using static Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions.InstGenHelper; +using static Ryujinx.Graphics.Shader.StructuredIr.InstructionInfo; + +namespace Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions +{ + static class InstGenBallot + { + public static string Ballot(CodeGenContext context, AstOperation operation) + { + AggregateType dstType = GetSrcVarType(operation.Inst, 0); + + string arg = GetSourceExpr(context, operation.GetSource(0), dstType); + char component = "xyzw"[operation.Index]; + + return $"uint4(as_type((simd_vote::vote_t)simd_ballot({arg})), 0, 0).{component}"; + } + + public static string VoteAllEqual(CodeGenContext context, AstOperation operation) + { + AggregateType dstType = GetSrcVarType(operation.Inst, 0); + + string arg = GetSourceExpr(context, operation.GetSource(0), dstType); + + return $"simd_all({arg}) || !simd_any({arg})"; + } + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenBarrier.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenBarrier.cs new file mode 100644 index 000000000..77f05defc --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenBarrier.cs @@ -0,0 +1,15 @@ +using Ryujinx.Graphics.Shader.IntermediateRepresentation; +using Ryujinx.Graphics.Shader.StructuredIr; + +namespace Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions +{ + static class InstGenBarrier + { + public static string Barrier(CodeGenContext context, AstOperation operation) + { + var device = (operation.Inst & Instruction.Mask) == Instruction.MemoryBarrier; + + return $"threadgroup_barrier(mem_flags::mem_{(device ? "device" : "threadgroup")})"; + } + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenCall.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenCall.cs new file mode 100644 index 000000000..44881deee --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenCall.cs @@ -0,0 +1,60 @@ +using Ryujinx.Graphics.Shader.StructuredIr; + +using static Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions.InstGenHelper; + +namespace Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions +{ + static class InstGenCall + { + public static string Call(CodeGenContext context, AstOperation operation) + { + AstOperand funcId = (AstOperand)operation.GetSource(0); + + var function = context.GetFunction(funcId.Value); + + int argCount = operation.SourcesCount - 1; + int additionalArgCount = CodeGenContext.AdditionalArgCount + (context.Definitions.Stage != ShaderStage.Compute ? 1 : 0); + bool needsThreadIndex = false; + + // TODO: Replace this with a proper flag + if (function.Name.Contains("Shuffle")) + { + needsThreadIndex = true; + additionalArgCount++; + } + + string[] args = new string[argCount + additionalArgCount]; + + // Additional arguments + if (context.Definitions.Stage != ShaderStage.Compute) + { + args[0] = "in"; + args[1] = "constant_buffers"; + args[2] = "storage_buffers"; + + if (needsThreadIndex) + { + args[3] = "thread_index_in_simdgroup"; + } + } + else + { + args[0] = "constant_buffers"; + args[1] = "storage_buffers"; + + if (needsThreadIndex) + { + args[2] = "thread_index_in_simdgroup"; + } + } + + int argIndex = additionalArgCount; + for (int i = 0; i < argCount; i++) + { + args[argIndex++] = GetSourceExpr(context, operation.GetSource(i + 1), function.GetArgumentType(i)); + } + + return $"{function.Name}({string.Join(", ", args)})"; + } + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenHelper.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenHelper.cs new file mode 100644 index 000000000..49f3c63aa --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenHelper.cs @@ -0,0 +1,222 @@ +using Ryujinx.Graphics.Shader.IntermediateRepresentation; +using Ryujinx.Graphics.Shader.StructuredIr; +using Ryujinx.Graphics.Shader.Translation; + +using static Ryujinx.Graphics.Shader.CodeGen.Msl.TypeConversion; + +namespace Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions +{ + static class InstGenHelper + { + private static readonly InstInfo[] _infoTable; + + static InstGenHelper() + { + _infoTable = new InstInfo[(int)Instruction.Count]; + +#pragma warning disable IDE0055 // Disable formatting + Add(Instruction.AtomicAdd, InstType.AtomicBinary, "atomic_fetch_add_explicit"); + Add(Instruction.AtomicAnd, InstType.AtomicBinary, "atomic_fetch_and_explicit"); + Add(Instruction.AtomicCompareAndSwap, InstType.AtomicBinary, "atomic_compare_exchange_weak_explicit"); + Add(Instruction.AtomicMaxU32, InstType.AtomicBinary, "atomic_fetch_max_explicit"); + Add(Instruction.AtomicMinU32, InstType.AtomicBinary, "atomic_fetch_min_explicit"); + Add(Instruction.AtomicOr, InstType.AtomicBinary, "atomic_fetch_or_explicit"); + Add(Instruction.AtomicSwap, InstType.AtomicBinary, "atomic_exchange_explicit"); + Add(Instruction.AtomicXor, InstType.AtomicBinary, "atomic_fetch_xor_explicit"); + Add(Instruction.Absolute, InstType.CallUnary, "abs"); + Add(Instruction.Add, InstType.OpBinaryCom, "+", 2); + Add(Instruction.Ballot, InstType.Special); + Add(Instruction.Barrier, InstType.Special); + Add(Instruction.BitCount, InstType.CallUnary, "popcount"); + Add(Instruction.BitfieldExtractS32, InstType.CallTernary, "extract_bits"); + Add(Instruction.BitfieldExtractU32, InstType.CallTernary, "extract_bits"); + Add(Instruction.BitfieldInsert, InstType.CallQuaternary, "insert_bits"); + Add(Instruction.BitfieldReverse, InstType.CallUnary, "reverse_bits"); + Add(Instruction.BitwiseAnd, InstType.OpBinaryCom, "&", 6); + Add(Instruction.BitwiseExclusiveOr, InstType.OpBinaryCom, "^", 7); + Add(Instruction.BitwiseNot, InstType.OpUnary, "~", 0); + Add(Instruction.BitwiseOr, InstType.OpBinaryCom, "|", 8); + Add(Instruction.Call, InstType.Special); + Add(Instruction.Ceiling, InstType.CallUnary, "ceil"); + Add(Instruction.Clamp, InstType.CallTernary, "clamp"); + Add(Instruction.ClampU32, InstType.CallTernary, "clamp"); + Add(Instruction.CompareEqual, InstType.OpBinaryCom, "==", 5); + Add(Instruction.CompareGreater, InstType.OpBinary, ">", 4); + Add(Instruction.CompareGreaterOrEqual, InstType.OpBinary, ">=", 4); + Add(Instruction.CompareGreaterOrEqualU32, InstType.OpBinary, ">=", 4); + Add(Instruction.CompareGreaterU32, InstType.OpBinary, ">", 4); + Add(Instruction.CompareLess, InstType.OpBinary, "<", 4); + Add(Instruction.CompareLessOrEqual, InstType.OpBinary, "<=", 4); + Add(Instruction.CompareLessOrEqualU32, InstType.OpBinary, "<=", 4); + Add(Instruction.CompareLessU32, InstType.OpBinary, "<", 4); + Add(Instruction.CompareNotEqual, InstType.OpBinaryCom, "!=", 5); + Add(Instruction.ConditionalSelect, InstType.OpTernary, "?:", 12); + Add(Instruction.ConvertFP32ToFP64, 0); // MSL does not have a 64-bit FP + Add(Instruction.ConvertFP64ToFP32, 0); // MSL does not have a 64-bit FP + Add(Instruction.ConvertFP32ToS32, InstType.CallUnary, "int"); + Add(Instruction.ConvertFP32ToU32, InstType.CallUnary, "uint"); + Add(Instruction.ConvertFP64ToS32, 0); // MSL does not have a 64-bit FP + Add(Instruction.ConvertFP64ToU32, 0); // MSL does not have a 64-bit FP + Add(Instruction.ConvertS32ToFP32, InstType.CallUnary, "float"); + Add(Instruction.ConvertS32ToFP64, 0); // MSL does not have a 64-bit FP + Add(Instruction.ConvertU32ToFP32, InstType.CallUnary, "float"); + Add(Instruction.ConvertU32ToFP64, 0); // MSL does not have a 64-bit FP + Add(Instruction.Cosine, InstType.CallUnary, "cos"); + Add(Instruction.Ddx, InstType.CallUnary, "dfdx"); + Add(Instruction.Ddy, InstType.CallUnary, "dfdy"); + Add(Instruction.Discard, InstType.CallNullary, "discard_fragment"); + Add(Instruction.Divide, InstType.OpBinary, "/", 1); + Add(Instruction.EmitVertex, 0); // MSL does not have geometry shaders + Add(Instruction.EndPrimitive, 0); // MSL does not have geometry shaders + Add(Instruction.ExponentB2, InstType.CallUnary, "exp2"); + Add(Instruction.FSIBegin, InstType.Special); + Add(Instruction.FSIEnd, InstType.Special); + Add(Instruction.FindLSB, InstType.CallUnary, HelperFunctionNames.FindLSB); + Add(Instruction.FindMSBS32, InstType.CallUnary, HelperFunctionNames.FindMSBS32); + Add(Instruction.FindMSBU32, InstType.CallUnary, HelperFunctionNames.FindMSBU32); + Add(Instruction.Floor, InstType.CallUnary, "floor"); + Add(Instruction.FusedMultiplyAdd, InstType.CallTernary, "fma"); + Add(Instruction.GroupMemoryBarrier, InstType.Special); + Add(Instruction.ImageLoad, InstType.Special); + Add(Instruction.ImageStore, InstType.Special); + Add(Instruction.ImageAtomic, InstType.Special); // Metal 3.1+ + Add(Instruction.IsNan, InstType.CallUnary, "isnan"); + Add(Instruction.Load, InstType.Special); + Add(Instruction.Lod, InstType.Special); + Add(Instruction.LogarithmB2, InstType.CallUnary, "log2"); + Add(Instruction.LogicalAnd, InstType.OpBinaryCom, "&&", 9); + Add(Instruction.LogicalExclusiveOr, InstType.OpBinaryCom, "^", 10); + Add(Instruction.LogicalNot, InstType.OpUnary, "!", 0); + Add(Instruction.LogicalOr, InstType.OpBinaryCom, "||", 11); + Add(Instruction.LoopBreak, InstType.OpNullary, "break"); + Add(Instruction.LoopContinue, InstType.OpNullary, "continue"); + Add(Instruction.PackDouble2x32, 0); // MSL does not have a 64-bit FP + Add(Instruction.PackHalf2x16, InstType.Special); + Add(Instruction.Maximum, InstType.CallBinary, "max"); + Add(Instruction.MaximumU32, InstType.CallBinary, "max"); + Add(Instruction.MemoryBarrier, InstType.Special); + Add(Instruction.Minimum, InstType.CallBinary, "min"); + Add(Instruction.MinimumU32, InstType.CallBinary, "min"); + Add(Instruction.Modulo, InstType.CallBinary, "fmod"); + Add(Instruction.Multiply, InstType.OpBinaryCom, "*", 1); + Add(Instruction.MultiplyHighS32, InstType.CallBinary, "mulhi"); + Add(Instruction.MultiplyHighU32, InstType.CallBinary, "mulhi"); + Add(Instruction.Negate, InstType.OpUnary, "-"); + Add(Instruction.ReciprocalSquareRoot, InstType.CallUnary, "rsqrt"); + Add(Instruction.Return, InstType.OpNullary, "return"); + Add(Instruction.Round, InstType.CallUnary, "round"); + Add(Instruction.ShiftLeft, InstType.OpBinary, "<<", 3); + Add(Instruction.ShiftRightS32, InstType.OpBinary, ">>", 3); + Add(Instruction.ShiftRightU32, InstType.OpBinary, ">>", 3); + Add(Instruction.Shuffle, InstType.CallBinary, "simd_shuffle"); + Add(Instruction.ShuffleDown, InstType.CallBinary, "simd_shuffle_down"); + Add(Instruction.ShuffleUp, InstType.CallBinary, "simd_shuffle_up"); + Add(Instruction.ShuffleXor, InstType.CallBinary, "simd_shuffle_xor"); + Add(Instruction.Sine, InstType.CallUnary, "sin"); + Add(Instruction.SquareRoot, InstType.CallUnary, "sqrt"); + Add(Instruction.Store, InstType.Special); + Add(Instruction.Subtract, InstType.OpBinary, "-", 2); + Add(Instruction.SwizzleAdd, InstType.CallTernary, HelperFunctionNames.SwizzleAdd); + Add(Instruction.TextureSample, InstType.Special); + Add(Instruction.TextureQuerySamples, InstType.Special); + Add(Instruction.TextureQuerySize, InstType.Special); + Add(Instruction.Truncate, InstType.CallUnary, "trunc"); + Add(Instruction.UnpackDouble2x32, 0); // MSL does not have a 64-bit FP + Add(Instruction.UnpackHalf2x16, InstType.Special); + Add(Instruction.VectorExtract, InstType.Special); + Add(Instruction.VoteAll, InstType.CallUnary, "simd_all"); + Add(Instruction.VoteAllEqual, InstType.Special); + Add(Instruction.VoteAny, InstType.CallUnary, "simd_any"); +#pragma warning restore IDE0055 + } + + private static void Add(Instruction inst, InstType flags, string opName = null, int precedence = 0) + { + _infoTable[(int)inst] = new InstInfo(flags, opName, precedence); + } + + public static InstInfo GetInstructionInfo(Instruction inst) + { + return _infoTable[(int)(inst & Instruction.Mask)]; + } + + public static string GetSourceExpr(CodeGenContext context, IAstNode node, AggregateType dstType) + { + return ReinterpretCast(context, node, OperandManager.GetNodeDestType(context, node), dstType); + } + + public static string Enclose(string expr, IAstNode node, Instruction pInst, bool isLhs) + { + InstInfo pInfo = GetInstructionInfo(pInst); + + return Enclose(expr, node, pInst, pInfo, isLhs); + } + + public static string Enclose(string expr, IAstNode node, Instruction pInst, InstInfo pInfo, bool isLhs = false) + { + if (NeedsParenthesis(node, pInst, pInfo, isLhs)) + { + expr = "(" + expr + ")"; + } + + return expr; + } + + public static bool NeedsParenthesis(IAstNode node, Instruction pInst, InstInfo pInfo, bool isLhs) + { + // If the node isn't an operation, then it can only be an operand, + // and those never needs to be surrounded in parentheses. + if (node is not AstOperation operation) + { + // This is sort of a special case, if this is a negative constant, + // and it is consumed by a unary operation, we need to put on the parenthesis, + // as in MSL, while a sequence like ~-1 is valid, --2 is not. + if (IsNegativeConst(node) && pInfo.Type == InstType.OpUnary) + { + return true; + } + + return false; + } + + if ((pInfo.Type & (InstType.Call | InstType.Special)) != 0) + { + return false; + } + + InstInfo info = _infoTable[(int)(operation.Inst & Instruction.Mask)]; + + if ((info.Type & (InstType.Call | InstType.Special)) != 0) + { + return false; + } + + if (info.Precedence < pInfo.Precedence) + { + return false; + } + + if (info.Precedence == pInfo.Precedence && isLhs) + { + return false; + } + + if (pInst == operation.Inst && info.Type == InstType.OpBinaryCom) + { + return false; + } + + return true; + } + + private static bool IsNegativeConst(IAstNode node) + { + if (node is not AstOperand operand) + { + return false; + } + + return operand.Type == OperandType.Constant && operand.Value < 0; + } + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenMemory.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenMemory.cs new file mode 100644 index 000000000..6ccacc1c4 --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenMemory.cs @@ -0,0 +1,672 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.Shader.IntermediateRepresentation; +using Ryujinx.Graphics.Shader.StructuredIr; +using Ryujinx.Graphics.Shader.Translation; +using System; +using System.Text; +using static Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions.InstGenHelper; +using static Ryujinx.Graphics.Shader.StructuredIr.InstructionInfo; + +namespace Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions +{ + static class InstGenMemory + { + public static string GenerateLoadOrStore(CodeGenContext context, AstOperation operation, bool isStore) + { + StorageKind storageKind = operation.StorageKind; + + string varName; + AggregateType varType; + int srcIndex = 0; + bool isStoreOrAtomic = operation.Inst == Instruction.Store || operation.Inst.IsAtomic(); + int inputsCount = isStoreOrAtomic ? operation.SourcesCount - 1 : operation.SourcesCount; + bool fieldHasPadding = false; + + if (operation.Inst == Instruction.AtomicCompareAndSwap) + { + inputsCount--; + } + + string fieldName = ""; + switch (storageKind) + { + case StorageKind.ConstantBuffer: + case StorageKind.StorageBuffer: + if (operation.GetSource(srcIndex++) is not AstOperand bindingIndex || bindingIndex.Type != OperandType.Constant) + { + throw new InvalidOperationException($"First input of {operation.Inst} with {storageKind} storage must be a constant operand."); + } + + int binding = bindingIndex.Value; + BufferDefinition buffer = storageKind == StorageKind.ConstantBuffer + ? context.Properties.ConstantBuffers[binding] + : context.Properties.StorageBuffers[binding]; + + if (operation.GetSource(srcIndex++) is not AstOperand fieldIndex || fieldIndex.Type != OperandType.Constant) + { + throw new InvalidOperationException($"Second input of {operation.Inst} with {storageKind} storage must be a constant operand."); + } + + StructureField field = buffer.Type.Fields[fieldIndex.Value]; + + fieldHasPadding = buffer.Layout == BufferLayout.Std140 + && ((field.Type & AggregateType.Vector4) == 0) + && ((field.Type & AggregateType.Array) != 0); + + varName = storageKind == StorageKind.ConstantBuffer + ? "constant_buffers" + : "storage_buffers"; + varName += "." + buffer.Name; + varName += "->" + field.Name; + varType = field.Type; + break; + + case StorageKind.LocalMemory: + case StorageKind.SharedMemory: + if (operation.GetSource(srcIndex++) is not AstOperand { Type: OperandType.Constant } bindingId) + { + throw new InvalidOperationException($"First input of {operation.Inst} with {storageKind} storage must be a constant operand."); + } + + MemoryDefinition memory = storageKind == StorageKind.LocalMemory + ? context.Properties.LocalMemories[bindingId.Value] + : context.Properties.SharedMemories[bindingId.Value]; + + varName = memory.Name; + varType = memory.Type; + break; + + case StorageKind.Input: + case StorageKind.InputPerPatch: + case StorageKind.Output: + case StorageKind.OutputPerPatch: + if (operation.GetSource(srcIndex++) is not AstOperand varId || varId.Type != OperandType.Constant) + { + throw new InvalidOperationException($"First input of {operation.Inst} with {storageKind} storage must be a constant operand."); + } + + IoVariable ioVariable = (IoVariable)varId.Value; + bool isOutput = storageKind.IsOutput(); + bool isPerPatch = storageKind.IsPerPatch(); + int location = -1; + int component = 0; + + if (context.Definitions.HasPerLocationInputOrOutput(ioVariable, isOutput)) + { + if (operation.GetSource(srcIndex++) is not AstOperand vecIndex || vecIndex.Type != OperandType.Constant) + { + throw new InvalidOperationException($"Second input of {operation.Inst} with {storageKind} storage must be a constant operand."); + } + + location = vecIndex.Value; + + if (operation.SourcesCount > srcIndex && + operation.GetSource(srcIndex) is AstOperand elemIndex && + elemIndex.Type == OperandType.Constant && + context.Definitions.HasPerLocationInputOrOutputComponent(ioVariable, vecIndex.Value, elemIndex.Value, isOutput)) + { + component = elemIndex.Value; + srcIndex++; + } + } + + (varName, varType) = IoMap.GetMslBuiltIn( + context.Definitions, + ioVariable, + location, + component, + isOutput, + isPerPatch); + break; + + default: + throw new InvalidOperationException($"Invalid storage kind {storageKind}."); + } + + for (; srcIndex < inputsCount; srcIndex++) + { + IAstNode src = operation.GetSource(srcIndex); + + if ((varType & AggregateType.ElementCountMask) != 0 && + srcIndex == inputsCount - 1 && + src is AstOperand elementIndex && + elementIndex.Type == OperandType.Constant) + { + varName += "." + "xyzw"[elementIndex.Value & 3]; + } + else + { + varName += $"[{GetSourceExpr(context, src, AggregateType.S32)}]"; + } + } + varName += fieldName; + varName += fieldHasPadding ? ".x" : ""; + + if (isStore) + { + varType &= AggregateType.ElementTypeMask; + varName = $"{varName} = {GetSourceExpr(context, operation.GetSource(srcIndex), varType)}"; + } + + return varName; + } + + public static string ImageLoadOrStore(CodeGenContext context, AstOperation operation) + { + AstTextureOperation texOp = (AstTextureOperation)operation; + + bool isArray = (texOp.Type & SamplerType.Array) != 0; + + var texCallBuilder = new StringBuilder(); + + int srcIndex = 0; + + string Src(AggregateType type) + { + return GetSourceExpr(context, texOp.GetSource(srcIndex++), type); + } + + string imageName = GetImageName(context, texOp, ref srcIndex); + texCallBuilder.Append(imageName); + texCallBuilder.Append('.'); + + if (texOp.Inst == Instruction.ImageAtomic) + { + texCallBuilder.Append((texOp.Flags & TextureFlags.AtomicMask) switch + { + TextureFlags.Add => "atomic_fetch_add", + TextureFlags.Minimum => "atomic_min", + TextureFlags.Maximum => "atomic_max", + TextureFlags.Increment => "atomic_fetch_add", + TextureFlags.Decrement => "atomic_fetch_sub", + TextureFlags.BitwiseAnd => "atomic_fetch_and", + TextureFlags.BitwiseOr => "atomic_fetch_or", + TextureFlags.BitwiseXor => "atomic_fetch_xor", + TextureFlags.Swap => "atomic_exchange", + TextureFlags.CAS => "atomic_compare_exchange_weak", + _ => "atomic_fetch_add", + }); + } + else + { + texCallBuilder.Append(texOp.Inst == Instruction.ImageLoad ? "read" : "write"); + } + + texCallBuilder.Append('('); + + var coordsBuilder = new StringBuilder(); + + int coordsCount = texOp.Type.GetDimensions(); + + if (coordsCount > 1) + { + string[] elems = new string[coordsCount]; + + for (int index = 0; index < coordsCount; index++) + { + elems[index] = Src(AggregateType.S32); + } + + coordsBuilder.Append($"uint{coordsCount}({string.Join(", ", elems)})"); + } + else + { + coordsBuilder.Append($"uint({Src(AggregateType.S32)})"); + } + + if (isArray) + { + coordsBuilder.Append(", "); + coordsBuilder.Append(Src(AggregateType.S32)); + } + + if (texOp.Inst == Instruction.ImageStore) + { + AggregateType type = texOp.Format.GetComponentType(); + + string[] cElems = new string[4]; + + for (int index = 0; index < 4; index++) + { + if (srcIndex < texOp.SourcesCount) + { + cElems[index] = Src(type); + } + else + { + cElems[index] = type switch + { + AggregateType.S32 => NumberFormatter.FormatInt(0), + AggregateType.U32 => NumberFormatter.FormatUint(0), + _ => NumberFormatter.FormatFloat(0), + }; + } + } + + string prefix = type switch + { + AggregateType.S32 => "int", + AggregateType.U32 => "uint", + AggregateType.FP32 => "float", + _ => string.Empty, + }; + + texCallBuilder.Append($"{prefix}4({string.Join(", ", cElems)})"); + texCallBuilder.Append(", "); + } + + texCallBuilder.Append(coordsBuilder); + + if (texOp.Inst == Instruction.ImageAtomic) + { + texCallBuilder.Append(", "); + + AggregateType type = texOp.Format.GetComponentType(); + + if ((texOp.Flags & TextureFlags.AtomicMask) == TextureFlags.CAS) + { + texCallBuilder.Append(Src(type)); // Compare value. + } + + string value = (texOp.Flags & TextureFlags.AtomicMask) switch + { + TextureFlags.Increment => NumberFormatter.FormatInt(1, type), // TODO: Clamp value + TextureFlags.Decrement => NumberFormatter.FormatInt(-1, type), // TODO: Clamp value + _ => Src(type), + }; + + texCallBuilder.Append(value); + // This doesn't match what the MSL spec document says so either + // it is wrong or the MSL compiler has a bug. + texCallBuilder.Append(")[0]"); + } + else + { + texCallBuilder.Append(')'); + + if (texOp.Inst == Instruction.ImageLoad) + { + texCallBuilder.Append(GetMaskMultiDest(texOp.Index)); + } + } + + return texCallBuilder.ToString(); + } + + public static string Load(CodeGenContext context, AstOperation operation) + { + return GenerateLoadOrStore(context, operation, isStore: false); + } + + public static string Lod(CodeGenContext context, AstOperation operation) + { + AstTextureOperation texOp = (AstTextureOperation)operation; + + int coordsCount = texOp.Type.GetDimensions(); + int coordsIndex = 0; + + string textureName = GetTextureName(context, texOp, ref coordsIndex); + string samplerName = GetSamplerName(context, texOp, ref coordsIndex); + + string coordsExpr; + + if (coordsCount > 1) + { + string[] elems = new string[coordsCount]; + + for (int index = 0; index < coordsCount; index++) + { + elems[index] = GetSourceExpr(context, texOp.GetSource(coordsIndex + index), AggregateType.FP32); + } + + coordsExpr = "float" + coordsCount + "(" + string.Join(", ", elems) + ")"; + } + else + { + coordsExpr = GetSourceExpr(context, texOp.GetSource(coordsIndex), AggregateType.FP32); + } + + var clamped = $"{textureName}.calculate_clamped_lod({samplerName}, {coordsExpr})"; + var unclamped = $"{textureName}.calculate_unclamped_lod({samplerName}, {coordsExpr})"; + + return $"float2({clamped}, {unclamped}){GetMask(texOp.Index)}"; + } + + public static string Store(CodeGenContext context, AstOperation operation) + { + return GenerateLoadOrStore(context, operation, isStore: true); + } + + public static string TextureSample(CodeGenContext context, AstOperation operation) + { + AstTextureOperation texOp = (AstTextureOperation)operation; + + bool isGather = (texOp.Flags & TextureFlags.Gather) != 0; + bool hasDerivatives = (texOp.Flags & TextureFlags.Derivatives) != 0; + bool intCoords = (texOp.Flags & TextureFlags.IntCoords) != 0; + bool hasLodBias = (texOp.Flags & TextureFlags.LodBias) != 0; + bool hasLodLevel = (texOp.Flags & TextureFlags.LodLevel) != 0; + bool hasOffset = (texOp.Flags & TextureFlags.Offset) != 0; + bool hasOffsets = (texOp.Flags & TextureFlags.Offsets) != 0; + + bool isArray = (texOp.Type & SamplerType.Array) != 0; + bool isShadow = (texOp.Type & SamplerType.Shadow) != 0; + + var texCallBuilder = new StringBuilder(); + + bool colorIsVector = isGather || !isShadow; + + int srcIndex = 0; + + string Src(AggregateType type) + { + return GetSourceExpr(context, texOp.GetSource(srcIndex++), type); + } + + string textureName = GetTextureName(context, texOp, ref srcIndex); + string samplerName = GetSamplerName(context, texOp, ref srcIndex); + + texCallBuilder.Append(textureName); + texCallBuilder.Append('.'); + + if (intCoords) + { + texCallBuilder.Append("read("); + } + else + { + if (isGather) + { + texCallBuilder.Append("gather"); + } + else + { + texCallBuilder.Append("sample"); + } + + if (isShadow) + { + texCallBuilder.Append("_compare"); + } + + texCallBuilder.Append($"({samplerName}, "); + } + + int coordsCount = texOp.Type.GetDimensions(); + + int pCount = coordsCount; + + bool appended = false; + void Append(string str) + { + if (appended) + { + texCallBuilder.Append(", "); + } + else + { + appended = true; + } + + texCallBuilder.Append(str); + } + + AggregateType coordType = intCoords ? AggregateType.S32 : AggregateType.FP32; + + string AssemblePVector(int count) + { + string coords; + if (count > 1) + { + string[] elems = new string[count]; + + for (int index = 0; index < count; index++) + { + elems[index] = Src(coordType); + } + + coords = string.Join(", ", elems); + } + else + { + coords = Src(coordType); + } + + string prefix = intCoords ? "uint" : "float"; + + return prefix + (count > 1 ? count : "") + "(" + coords + ")"; + } + + Append(AssemblePVector(pCount)); + + if (isArray) + { + Append(Src(AggregateType.S32)); + } + + if (isShadow) + { + Append(Src(AggregateType.FP32)); + } + + if (hasDerivatives) + { + Logger.Warning?.PrintMsg(LogClass.Gpu, "Unused sampler derivatives!"); + } + + if (hasLodBias) + { + Logger.Warning?.PrintMsg(LogClass.Gpu, "Unused sample LOD bias!"); + } + + if (hasLodLevel) + { + if (intCoords) + { + Append(Src(coordType)); + } + else + { + Append($"level({Src(coordType)})"); + } + } + + string AssembleOffsetVector(int count) + { + if (count > 1) + { + string[] elems = new string[count]; + + for (int index = 0; index < count; index++) + { + elems[index] = Src(AggregateType.S32); + } + + return "int" + count + "(" + string.Join(", ", elems) + ")"; + } + else + { + return Src(AggregateType.S32); + } + } + + // TODO: Support reads with offsets + if (!intCoords) + { + if (hasOffset) + { + Append(AssembleOffsetVector(coordsCount)); + } + else if (hasOffsets) + { + Logger.Warning?.PrintMsg(LogClass.Gpu, "Multiple offsets on gathers are not yet supported!"); + } + } + + texCallBuilder.Append(')'); + texCallBuilder.Append(colorIsVector ? GetMaskMultiDest(texOp.Index) : ""); + + return texCallBuilder.ToString(); + } + + private static string GetTextureName(CodeGenContext context, AstTextureOperation texOp, ref int srcIndex) + { + TextureDefinition textureDefinition = context.Properties.Textures[texOp.GetTextureSetAndBinding()]; + string name = textureDefinition.Name; + string setName = Declarations.GetNameForSet(textureDefinition.Set, true); + + if (textureDefinition.ArrayLength != 1) + { + name = $"{name}[{GetSourceExpr(context, texOp.GetSource(srcIndex++), AggregateType.S32)}]"; + } + + return $"{setName}.tex_{name}"; + } + + private static string GetSamplerName(CodeGenContext context, AstTextureOperation texOp, ref int srcIndex) + { + var index = texOp.IsSeparate ? texOp.GetSamplerSetAndBinding() : texOp.GetTextureSetAndBinding(); + var sourceIndex = texOp.IsSeparate ? srcIndex++ : srcIndex + 1; + + TextureDefinition samplerDefinition = context.Properties.Textures[index]; + string name = samplerDefinition.Name; + string setName = Declarations.GetNameForSet(samplerDefinition.Set, true); + + if (samplerDefinition.ArrayLength != 1) + { + name = $"{name}[{GetSourceExpr(context, texOp.GetSource(sourceIndex), AggregateType.S32)}]"; + } + + return $"{setName}.samp_{name}"; + } + + private static string GetImageName(CodeGenContext context, AstTextureOperation texOp, ref int srcIndex) + { + TextureDefinition imageDefinition = context.Properties.Images[texOp.GetTextureSetAndBinding()]; + string name = imageDefinition.Name; + string setName = Declarations.GetNameForSet(imageDefinition.Set, true); + + if (imageDefinition.ArrayLength != 1) + { + name = $"{name}[{GetSourceExpr(context, texOp.GetSource(srcIndex++), AggregateType.S32)}]"; + } + + return $"{setName}.{name}"; + } + + private static string GetMaskMultiDest(int mask) + { + if (mask == 0x0) + { + return ""; + } + + string swizzle = "."; + + for (int i = 0; i < 4; i++) + { + if ((mask & (1 << i)) != 0) + { + swizzle += "xyzw"[i]; + } + } + + return swizzle; + } + + public static string TextureQuerySamples(CodeGenContext context, AstOperation operation) + { + AstTextureOperation texOp = (AstTextureOperation)operation; + + int srcIndex = 0; + + string textureName = GetTextureName(context, texOp, ref srcIndex); + + return $"{textureName}.get_num_samples()"; + } + + public static string TextureQuerySize(CodeGenContext context, AstOperation operation) + { + AstTextureOperation texOp = (AstTextureOperation)operation; + + var texCallBuilder = new StringBuilder(); + + int srcIndex = 0; + + string textureName = GetTextureName(context, texOp, ref srcIndex); + texCallBuilder.Append(textureName); + texCallBuilder.Append('.'); + + if (texOp.Index == 3) + { + texCallBuilder.Append("get_num_mip_levels()"); + } + else + { + context.Properties.Textures.TryGetValue(texOp.GetTextureSetAndBinding(), out TextureDefinition definition); + bool hasLod = !definition.Type.HasFlag(SamplerType.Multisample) && (definition.Type & SamplerType.Mask) != SamplerType.TextureBuffer; + bool isArray = definition.Type.HasFlag(SamplerType.Array); + texCallBuilder.Append("get_"); + + if (texOp.Index == 0) + { + texCallBuilder.Append("width"); + } + else if (texOp.Index == 1) + { + texCallBuilder.Append("height"); + } + else + { + if (isArray) + { + texCallBuilder.Append("array_size"); + } + else + { + texCallBuilder.Append("depth"); + } + } + + texCallBuilder.Append('('); + + if (hasLod && !isArray) + { + IAstNode lod = operation.GetSource(0); + string lodExpr = GetSourceExpr(context, lod, GetSrcVarType(operation.Inst, 0)); + + texCallBuilder.Append(lodExpr); + } + + texCallBuilder.Append(')'); + } + + return texCallBuilder.ToString(); + } + + public static string PackHalf2x16(CodeGenContext context, AstOperation operation) + { + IAstNode src0 = operation.GetSource(0); + IAstNode src1 = operation.GetSource(1); + + string src0Expr = GetSourceExpr(context, src0, GetSrcVarType(operation.Inst, 0)); + string src1Expr = GetSourceExpr(context, src1, GetSrcVarType(operation.Inst, 1)); + + return $"as_type(half2({src0Expr}, {src1Expr}))"; + } + + public static string UnpackHalf2x16(CodeGenContext context, AstOperation operation) + { + IAstNode src = operation.GetSource(0); + + string srcExpr = GetSourceExpr(context, src, GetSrcVarType(operation.Inst, 0)); + + return $"float2(as_type({srcExpr})){GetMask(operation.Index)}"; + } + + private static string GetMask(int index) + { + return $".{"xy".AsSpan(index, 1)}"; + } + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenVector.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenVector.cs new file mode 100644 index 000000000..9d8dae543 --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGenVector.cs @@ -0,0 +1,32 @@ +using Ryujinx.Graphics.Shader.IntermediateRepresentation; +using Ryujinx.Graphics.Shader.StructuredIr; + +using static Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions.InstGenHelper; +using static Ryujinx.Graphics.Shader.StructuredIr.InstructionInfo; + +namespace Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions +{ + static class InstGenVector + { + public static string VectorExtract(CodeGenContext context, AstOperation operation) + { + IAstNode vector = operation.GetSource(0); + IAstNode index = operation.GetSource(1); + + string vectorExpr = GetSourceExpr(context, vector, OperandManager.GetNodeDestType(context, vector)); + + if (index is AstOperand indexOperand && indexOperand.Type == OperandType.Constant) + { + char elem = "xyzw"[indexOperand.Value]; + + return $"{vectorExpr}.{elem}"; + } + else + { + string indexExpr = GetSourceExpr(context, index, GetSrcVarType(operation.Inst, 1)); + + return $"{vectorExpr}[{indexExpr}]"; + } + } + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstInfo.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstInfo.cs new file mode 100644 index 000000000..5e5d04d6b --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstInfo.cs @@ -0,0 +1,18 @@ +namespace Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions +{ + readonly struct InstInfo + { + public InstType Type { get; } + + public string OpName { get; } + + public int Precedence { get; } + + public InstInfo(InstType type, string opName, int precedence) + { + Type = type; + OpName = opName; + Precedence = precedence; + } + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstType.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstType.cs new file mode 100644 index 000000000..d8f6bfed1 --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstType.cs @@ -0,0 +1,35 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions +{ + [Flags] + [SuppressMessage("Design", "CA1069: Enums values should not be duplicated")] + public enum InstType + { + OpNullary = Op | 0, + OpUnary = Op | 1, + OpBinary = Op | 2, + OpBinaryCom = Op | 2 | Commutative, + OpTernary = Op | 3, + + CallNullary = Call | 0, + CallUnary = Call | 1, + CallBinary = Call | 2, + CallTernary = Call | 3, + CallQuaternary = Call | 4, + + // The atomic instructions have one extra operand, + // for the storage slot and offset pair. + AtomicBinary = Call | Atomic | 3, + AtomicTernary = Call | Atomic | 4, + + Commutative = 1 << 8, + Op = 1 << 9, + Call = 1 << 10, + Atomic = 1 << 11, + Special = 1 << 12, + + ArityMask = 0xff, + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/IoMap.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/IoMap.cs new file mode 100644 index 000000000..e02d0a61f --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/IoMap.cs @@ -0,0 +1,83 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.Shader.IntermediateRepresentation; +using Ryujinx.Graphics.Shader.Translation; +using System.Globalization; + +namespace Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions +{ + static class IoMap + { + public static (string, AggregateType) GetMslBuiltIn( + ShaderDefinitions definitions, + IoVariable ioVariable, + int location, + int component, + bool isOutput, + bool isPerPatch) + { + var returnValue = ioVariable switch + { + IoVariable.BaseInstance => ("base_instance", AggregateType.U32), + IoVariable.BaseVertex => ("base_vertex", AggregateType.U32), + IoVariable.CtaId => ("threadgroup_position_in_grid", AggregateType.Vector3 | AggregateType.U32), + IoVariable.ClipDistance => ("out.clip_distance", AggregateType.Array | AggregateType.FP32), + IoVariable.FragmentOutputColor => ($"out.color{location}", definitions.GetFragmentOutputColorType(location)), + IoVariable.FragmentOutputDepth => ("out.depth", AggregateType.FP32), + IoVariable.FrontFacing => ("in.front_facing", AggregateType.Bool), + IoVariable.GlobalId => ("thread_position_in_grid", AggregateType.Vector3 | AggregateType.U32), + IoVariable.InstanceId => ("instance_id", AggregateType.U32), + IoVariable.InstanceIndex => ("instance_index", AggregateType.U32), + IoVariable.InvocationId => ("INVOCATION_ID", AggregateType.S32), + IoVariable.PointCoord => ("in.point_coord", AggregateType.Vector2 | AggregateType.FP32), + IoVariable.PointSize => ("out.point_size", AggregateType.FP32), + IoVariable.Position => ("out.position", AggregateType.Vector4 | AggregateType.FP32), + IoVariable.PrimitiveId => ("in.primitive_id", AggregateType.U32), + IoVariable.SubgroupEqMask => ("thread_index_in_simdgroup >= 32 ? uint4(0, (1 << (thread_index_in_simdgroup - 32)), uint2(0)) : uint4(1 << thread_index_in_simdgroup, uint3(0))", AggregateType.Vector4 | AggregateType.U32), + IoVariable.SubgroupGeMask => ("uint4(insert_bits(0u, 0xFFFFFFFF, thread_index_in_simdgroup, 32 - thread_index_in_simdgroup), uint3(0)) & (uint4((uint)((simd_vote::vote_t)simd_ballot(true) & 0xFFFFFFFF), (uint)(((simd_vote::vote_t)simd_ballot(true) >> 32) & 0xFFFFFFFF), 0, 0))", AggregateType.Vector4 | AggregateType.U32), + IoVariable.SubgroupGtMask => ("uint4(insert_bits(0u, 0xFFFFFFFF, thread_index_in_simdgroup + 1, 32 - thread_index_in_simdgroup - 1), uint3(0)) & (uint4((uint)((simd_vote::vote_t)simd_ballot(true) & 0xFFFFFFFF), (uint)(((simd_vote::vote_t)simd_ballot(true) >> 32) & 0xFFFFFFFF), 0, 0))", AggregateType.Vector4 | AggregateType.U32), + IoVariable.SubgroupLaneId => ("thread_index_in_simdgroup", AggregateType.U32), + IoVariable.SubgroupLeMask => ("uint4(extract_bits(0xFFFFFFFF, 0, min(thread_index_in_simdgroup + 1, 32u)), extract_bits(0xFFFFFFFF, 0, (uint)max((int)thread_index_in_simdgroup + 1 - 32, 0)), uint2(0))", AggregateType.Vector4 | AggregateType.U32), + IoVariable.SubgroupLtMask => ("uint4(extract_bits(0xFFFFFFFF, 0, min(thread_index_in_simdgroup, 32u)), extract_bits(0xFFFFFFFF, 0, (uint)max((int)thread_index_in_simdgroup - 32, 0)), uint2(0))", AggregateType.Vector4 | AggregateType.U32), + IoVariable.ThreadKill => ("simd_is_helper_thread()", AggregateType.Bool), + IoVariable.UserDefined => GetUserDefinedVariableName(definitions, location, component, isOutput, isPerPatch), + IoVariable.ThreadId => ("thread_position_in_threadgroup", AggregateType.Vector3 | AggregateType.U32), + IoVariable.VertexId => ("vertex_id", AggregateType.S32), + // gl_VertexIndex does not have a direct equivalent in MSL + IoVariable.VertexIndex => ("vertex_id", AggregateType.U32), + IoVariable.ViewportIndex => ("viewport_array_index", AggregateType.S32), + IoVariable.FragmentCoord => ("in.position", AggregateType.Vector4 | AggregateType.FP32), + _ => (null, AggregateType.Invalid), + }; + + if (returnValue.Item2 == AggregateType.Invalid) + { + Logger.Warning?.PrintMsg(LogClass.Gpu, $"Unable to find type for IoVariable {ioVariable}!"); + } + + return returnValue; + } + + private static (string, AggregateType) GetUserDefinedVariableName(ShaderDefinitions definitions, int location, int component, bool isOutput, bool isPerPatch) + { + string name = isPerPatch + ? Defaults.PerPatchAttributePrefix + : (isOutput ? Defaults.OAttributePrefix : Defaults.IAttributePrefix); + + if (location < 0) + { + return (name, definitions.GetUserDefinedType(0, isOutput)); + } + + name += location.ToString(CultureInfo.InvariantCulture); + + if (definitions.HasPerLocationInputOrOutputComponent(IoVariable.UserDefined, location, component, isOutput)) + { + name += "_" + "xyzw"[component & 3]; + } + + string prefix = isOutput ? "out" : "in"; + + return (prefix + "." + name, definitions.GetUserDefinedType(location, isOutput)); + } + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/MslGenerator.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/MslGenerator.cs new file mode 100644 index 000000000..7de6ee5dd --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/MslGenerator.cs @@ -0,0 +1,286 @@ +using Ryujinx.Common.Logging; +using Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions; +using Ryujinx.Graphics.Shader.StructuredIr; +using Ryujinx.Graphics.Shader.Translation; +using System; +using System.Linq; +using static Ryujinx.Graphics.Shader.CodeGen.Msl.TypeConversion; + +namespace Ryujinx.Graphics.Shader.CodeGen.Msl +{ + static class MslGenerator + { + public static string Generate(StructuredProgramInfo info, CodeGenParameters parameters) + { + if (parameters.Definitions.Stage is not (ShaderStage.Vertex or ShaderStage.Fragment or ShaderStage.Compute)) + { + Logger.Warning?.Print(LogClass.Gpu, $"Attempted to generate unsupported shader type {parameters.Definitions.Stage}!"); + return ""; + } + + CodeGenContext context = new(info, parameters); + + var sets = Declarations.Declare(context, info); + + if (info.Functions.Count != 0) + { + for (int i = 1; i < info.Functions.Count; i++) + { + PrintFunction(context, info.Functions[i], parameters.Definitions.Stage, sets); + + context.AppendLine(); + } + } + + PrintFunction(context, info.Functions[0], parameters.Definitions.Stage, sets, true); + + return context.GetCode(); + } + + private static void PrintFunction(CodeGenContext context, StructuredFunction function, ShaderStage stage, int[] sets, bool isMainFunc = false) + { + context.CurrentFunction = function; + + context.AppendLine(GetFunctionSignature(context, function, stage, sets, isMainFunc)); + context.EnterScope(); + + Declarations.DeclareLocals(context, function, stage, isMainFunc); + + PrintBlock(context, function.MainBlock, isMainFunc); + + // In case the shader hasn't returned, return + if (isMainFunc && stage != ShaderStage.Compute) + { + context.AppendLine("return out;"); + } + + context.LeaveScope(); + } + + private static string GetFunctionSignature( + CodeGenContext context, + StructuredFunction function, + ShaderStage stage, + int[] sets, + bool isMainFunc = false) + { + int additionalArgCount = isMainFunc ? 0 : CodeGenContext.AdditionalArgCount + (context.Definitions.Stage != ShaderStage.Compute ? 1 : 0); + bool needsThreadIndex = false; + + // TODO: Replace this with a proper flag + if (function.Name.Contains("Shuffle")) + { + needsThreadIndex = true; + additionalArgCount++; + } + + string[] args = new string[additionalArgCount + function.InArguments.Length + function.OutArguments.Length]; + + // All non-main functions need to be able to access the support_buffer as well + if (!isMainFunc) + { + if (stage != ShaderStage.Compute) + { + args[0] = stage == ShaderStage.Vertex ? "VertexIn in" : "FragmentIn in"; + args[1] = "constant ConstantBuffers &constant_buffers"; + args[2] = "device StorageBuffers &storage_buffers"; + + if (needsThreadIndex) + { + args[3] = "uint thread_index_in_simdgroup"; + } + } + else + { + args[0] = "constant ConstantBuffers &constant_buffers"; + args[1] = "device StorageBuffers &storage_buffers"; + + if (needsThreadIndex) + { + args[2] = "uint thread_index_in_simdgroup"; + } + } + } + + int argIndex = additionalArgCount; + for (int i = 0; i < function.InArguments.Length; i++) + { + args[argIndex++] = $"{Declarations.GetVarTypeName(function.InArguments[i])} {OperandManager.GetArgumentName(i)}"; + } + + for (int i = 0; i < function.OutArguments.Length; i++) + { + int j = i + function.InArguments.Length; + + args[argIndex++] = $"thread {Declarations.GetVarTypeName(function.OutArguments[i])} &{OperandManager.GetArgumentName(j)}"; + } + + string funcKeyword = "inline"; + string funcName = null; + string returnType = Declarations.GetVarTypeName(function.ReturnType); + + if (isMainFunc) + { + if (stage == ShaderStage.Vertex) + { + funcKeyword = "vertex"; + funcName = "vertexMain"; + returnType = "VertexOut"; + } + else if (stage == ShaderStage.Fragment) + { + funcKeyword = "fragment"; + funcName = "fragmentMain"; + returnType = "FragmentOut"; + } + else if (stage == ShaderStage.Compute) + { + funcKeyword = "kernel"; + funcName = "kernelMain"; + returnType = "void"; + } + + if (stage == ShaderStage.Vertex) + { + args = args.Prepend("VertexIn in [[stage_in]]").ToArray(); + } + else if (stage == ShaderStage.Fragment) + { + args = args.Prepend("FragmentIn in [[stage_in]]").ToArray(); + } + + // TODO: add these only if they are used + if (stage == ShaderStage.Vertex) + { + args = args.Append("uint vertex_id [[vertex_id]]").ToArray(); + args = args.Append("uint instance_id [[instance_id]]").ToArray(); + args = args.Append("uint base_instance [[base_instance]]").ToArray(); + args = args.Append("uint base_vertex [[base_vertex]]").ToArray(); + } + else if (stage == ShaderStage.Compute) + { + args = args.Append("uint3 threadgroup_position_in_grid [[threadgroup_position_in_grid]]").ToArray(); + args = args.Append("uint3 thread_position_in_grid [[thread_position_in_grid]]").ToArray(); + args = args.Append("uint3 thread_position_in_threadgroup [[thread_position_in_threadgroup]]").ToArray(); + args = args.Append("uint thread_index_in_simdgroup [[thread_index_in_simdgroup]]").ToArray(); + } + + args = args.Append($"constant ConstantBuffers &constant_buffers [[buffer({Defaults.ConstantBuffersIndex})]]").ToArray(); + args = args.Append($"device StorageBuffers &storage_buffers [[buffer({Defaults.StorageBuffersIndex})]]").ToArray(); + + foreach (var set in sets) + { + var bindingIndex = set + Defaults.BaseSetIndex; + args = args.Append($"constant {Declarations.GetNameForSet(set)} &{Declarations.GetNameForSet(set, true)} [[buffer({bindingIndex})]]").ToArray(); + } + } + + var funcPrefix = $"{funcKeyword} {returnType} {funcName ?? function.Name}("; + var indent = new string(' ', funcPrefix.Length); + + return $"{funcPrefix}{string.Join($", \n{indent}", args)})"; + } + + private static void PrintBlock(CodeGenContext context, AstBlock block, bool isMainFunction) + { + AstBlockVisitor visitor = new(block); + + visitor.BlockEntered += (sender, e) => + { + switch (e.Block.Type) + { + case AstBlockType.DoWhile: + context.AppendLine("do"); + break; + + case AstBlockType.Else: + context.AppendLine("else"); + break; + + case AstBlockType.ElseIf: + context.AppendLine($"else if ({GetCondExpr(context, e.Block.Condition)})"); + break; + + case AstBlockType.If: + context.AppendLine($"if ({GetCondExpr(context, e.Block.Condition)})"); + break; + + default: + throw new InvalidOperationException($"Found unexpected block type \"{e.Block.Type}\"."); + } + + context.EnterScope(); + }; + + visitor.BlockLeft += (sender, e) => + { + context.LeaveScope(); + + if (e.Block.Type == AstBlockType.DoWhile) + { + context.AppendLine($"while ({GetCondExpr(context, e.Block.Condition)});"); + } + }; + + bool supportsBarrierDivergence = context.HostCapabilities.SupportsShaderBarrierDivergence; + bool mayHaveReturned = false; + + foreach (IAstNode node in visitor.Visit()) + { + if (node is AstOperation operation) + { + if (!supportsBarrierDivergence) + { + if (operation.Inst == IntermediateRepresentation.Instruction.Barrier) + { + // Barrier on divergent control flow paths may cause the GPU to hang, + // so skip emitting the barrier for those cases. + if (visitor.Block.Type != AstBlockType.Main || mayHaveReturned || !isMainFunction) + { + context.Logger.Log($"Shader has barrier on potentially divergent block, the barrier will be removed."); + + continue; + } + } + else if (operation.Inst == IntermediateRepresentation.Instruction.Return) + { + mayHaveReturned = true; + } + } + + string expr = InstGen.GetExpression(context, operation); + + if (expr != null) + { + context.AppendLine(expr + ";"); + } + } + else if (node is AstAssignment assignment) + { + AggregateType dstType = OperandManager.GetNodeDestType(context, assignment.Destination); + AggregateType srcType = OperandManager.GetNodeDestType(context, assignment.Source); + + string dest = InstGen.GetExpression(context, assignment.Destination); + string src = ReinterpretCast(context, assignment.Source, srcType, dstType); + + context.AppendLine(dest + " = " + src + ";"); + } + else if (node is AstComment comment) + { + context.AppendLine("// " + comment.Comment); + } + else + { + throw new InvalidOperationException($"Found unexpected node type \"{node?.GetType().Name ?? "null"}\"."); + } + } + } + + private static string GetCondExpr(CodeGenContext context, IAstNode cond) + { + AggregateType srcType = OperandManager.GetNodeDestType(context, cond); + + return ReinterpretCast(context, cond, srcType, AggregateType.Bool); + } + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/NumberFormatter.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/NumberFormatter.cs new file mode 100644 index 000000000..86cdfc0e6 --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/NumberFormatter.cs @@ -0,0 +1,94 @@ +using Ryujinx.Graphics.Shader.Translation; +using System; +using System.Globalization; + +namespace Ryujinx.Graphics.Shader.CodeGen.Msl +{ + static class NumberFormatter + { + private const int MaxDecimal = 256; + + public static bool TryFormat(int value, AggregateType dstType, out string formatted) + { + switch (dstType) + { + case AggregateType.FP32: + return TryFormatFloat(BitConverter.Int32BitsToSingle(value), out formatted); + case AggregateType.S32: + formatted = FormatInt(value); + break; + case AggregateType.U32: + formatted = FormatUint((uint)value); + break; + case AggregateType.Bool: + formatted = value != 0 ? "true" : "false"; + break; + default: + throw new ArgumentException($"Invalid variable type \"{dstType}\"."); + } + + return true; + } + + public static string FormatFloat(float value) + { + if (!TryFormatFloat(value, out string formatted)) + { + throw new ArgumentException("Failed to convert float value to string."); + } + + return formatted; + } + + public static bool TryFormatFloat(float value, out string formatted) + { + if (float.IsNaN(value) || float.IsInfinity(value)) + { + formatted = null; + + return false; + } + + formatted = value.ToString("G9", CultureInfo.InvariantCulture); + + if (!(formatted.Contains('.') || + formatted.Contains('e') || + formatted.Contains('E'))) + { + formatted += ".0f"; + } + + return true; + } + + public static string FormatInt(int value, AggregateType dstType) + { + return dstType switch + { + AggregateType.S32 => FormatInt(value), + AggregateType.U32 => FormatUint((uint)value), + _ => throw new ArgumentException($"Invalid variable type \"{dstType}\".") + }; + } + + public static string FormatInt(int value) + { + if (value <= MaxDecimal && value >= -MaxDecimal) + { + return value.ToString(CultureInfo.InvariantCulture); + } + + return $"as_type(0x{value.ToString("X", CultureInfo.InvariantCulture)})"; + } + + public static string FormatUint(uint value) + { + if (value <= MaxDecimal && value >= 0) + { + return value.ToString(CultureInfo.InvariantCulture) + "u"; + } + + return $"as_type(0x{value.ToString("X", CultureInfo.InvariantCulture)})"; + } + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/OperandManager.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/OperandManager.cs new file mode 100644 index 000000000..e131a645e --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/OperandManager.cs @@ -0,0 +1,176 @@ +using Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions; +using Ryujinx.Graphics.Shader.IntermediateRepresentation; +using Ryujinx.Graphics.Shader.StructuredIr; +using Ryujinx.Graphics.Shader.Translation; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using static Ryujinx.Graphics.Shader.StructuredIr.InstructionInfo; + +namespace Ryujinx.Graphics.Shader.CodeGen.Msl +{ + class OperandManager + { + private readonly Dictionary _locals; + + public OperandManager() + { + _locals = new Dictionary(); + } + + public string DeclareLocal(AstOperand operand) + { + string name = $"{Defaults.LocalNamePrefix}_{_locals.Count}"; + + _locals.Add(operand, name); + + return name; + } + + public string GetExpression(CodeGenContext context, AstOperand operand) + { + return operand.Type switch + { + OperandType.Argument => GetArgumentName(operand.Value), + OperandType.Constant => NumberFormatter.FormatInt(operand.Value), + OperandType.LocalVariable => _locals[operand], + OperandType.Undefined => Defaults.UndefinedName, + _ => throw new ArgumentException($"Invalid operand type \"{operand.Type}\"."), + }; + } + + public static string GetArgumentName(int argIndex) + { + return $"{Defaults.ArgumentNamePrefix}{argIndex}"; + } + + public static AggregateType GetNodeDestType(CodeGenContext context, IAstNode node) + { + if (node is AstOperation operation) + { + if (operation.Inst == Instruction.Load || operation.Inst.IsAtomic()) + { + switch (operation.StorageKind) + { + case StorageKind.ConstantBuffer: + case StorageKind.StorageBuffer: + if (operation.GetSource(0) is not AstOperand bindingIndex || bindingIndex.Type != OperandType.Constant) + { + throw new InvalidOperationException($"First input of {operation.Inst} with {operation.StorageKind} storage must be a constant operand."); + } + + if (operation.GetSource(1) is not AstOperand fieldIndex || fieldIndex.Type != OperandType.Constant) + { + throw new InvalidOperationException($"Second input of {operation.Inst} with {operation.StorageKind} storage must be a constant operand."); + } + + BufferDefinition buffer = operation.StorageKind == StorageKind.ConstantBuffer + ? context.Properties.ConstantBuffers[bindingIndex.Value] + : context.Properties.StorageBuffers[bindingIndex.Value]; + StructureField field = buffer.Type.Fields[fieldIndex.Value]; + + return field.Type & AggregateType.ElementTypeMask; + + case StorageKind.LocalMemory: + case StorageKind.SharedMemory: + if (operation.GetSource(0) is not AstOperand { Type: OperandType.Constant } bindingId) + { + throw new InvalidOperationException($"First input of {operation.Inst} with {operation.StorageKind} storage must be a constant operand."); + } + + MemoryDefinition memory = operation.StorageKind == StorageKind.LocalMemory + ? context.Properties.LocalMemories[bindingId.Value] + : context.Properties.SharedMemories[bindingId.Value]; + + return memory.Type & AggregateType.ElementTypeMask; + + case StorageKind.Input: + case StorageKind.InputPerPatch: + case StorageKind.Output: + case StorageKind.OutputPerPatch: + if (operation.GetSource(0) is not AstOperand varId || varId.Type != OperandType.Constant) + { + throw new InvalidOperationException($"First input of {operation.Inst} with {operation.StorageKind} storage must be a constant operand."); + } + + IoVariable ioVariable = (IoVariable)varId.Value; + bool isOutput = operation.StorageKind == StorageKind.Output || operation.StorageKind == StorageKind.OutputPerPatch; + bool isPerPatch = operation.StorageKind == StorageKind.InputPerPatch || operation.StorageKind == StorageKind.OutputPerPatch; + int location = 0; + int component = 0; + + if (context.Definitions.HasPerLocationInputOrOutput(ioVariable, isOutput)) + { + if (operation.GetSource(1) is not AstOperand vecIndex || vecIndex.Type != OperandType.Constant) + { + throw new InvalidOperationException($"Second input of {operation.Inst} with {operation.StorageKind} storage must be a constant operand."); + } + + location = vecIndex.Value; + + if (operation.SourcesCount > 2 && + operation.GetSource(2) is AstOperand elemIndex && + elemIndex.Type == OperandType.Constant && + context.Definitions.HasPerLocationInputOrOutputComponent(ioVariable, location, elemIndex.Value, isOutput)) + { + component = elemIndex.Value; + } + } + + (_, AggregateType varType) = IoMap.GetMslBuiltIn( + context.Definitions, + ioVariable, + location, + component, + isOutput, + isPerPatch); + + return varType & AggregateType.ElementTypeMask; + } + } + else if (operation.Inst == Instruction.Call) + { + AstOperand funcId = (AstOperand)operation.GetSource(0); + + Debug.Assert(funcId.Type == OperandType.Constant); + + return context.GetFunction(funcId.Value).ReturnType; + } + else if (operation.Inst == Instruction.VectorExtract) + { + return GetNodeDestType(context, operation.GetSource(0)) & ~AggregateType.ElementCountMask; + } + else if (operation is AstTextureOperation texOp) + { + if (texOp.Inst == Instruction.ImageLoad || + texOp.Inst == Instruction.ImageStore || + texOp.Inst == Instruction.ImageAtomic) + { + return texOp.GetVectorType(texOp.Format.GetComponentType()); + } + else if (texOp.Inst == Instruction.TextureSample) + { + return texOp.GetVectorType(GetDestVarType(operation.Inst)); + } + } + + return GetDestVarType(operation.Inst); + } + else if (node is AstOperand operand) + { + if (operand.Type == OperandType.Argument) + { + int argIndex = operand.Value; + + return context.CurrentFunction.GetArgumentType(argIndex); + } + + return OperandInfo.GetVarType(operand); + } + else + { + throw new ArgumentException($"Invalid node type \"{node?.GetType().Name ?? "null"}\"."); + } + } + } +} diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/TypeConversion.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/TypeConversion.cs new file mode 100644 index 000000000..e145bb8b0 --- /dev/null +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/TypeConversion.cs @@ -0,0 +1,93 @@ +using Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions; +using Ryujinx.Graphics.Shader.IntermediateRepresentation; +using Ryujinx.Graphics.Shader.StructuredIr; +using Ryujinx.Graphics.Shader.Translation; +using System; + +namespace Ryujinx.Graphics.Shader.CodeGen.Msl +{ + static class TypeConversion + { + public static string ReinterpretCast( + CodeGenContext context, + IAstNode node, + AggregateType srcType, + AggregateType dstType) + { + if (node is AstOperand operand && operand.Type == OperandType.Constant) + { + if (NumberFormatter.TryFormat(operand.Value, dstType, out string formatted)) + { + return formatted; + } + } + + string expr = InstGen.GetExpression(context, node); + + return ReinterpretCast(expr, node, srcType, dstType); + } + + private static string ReinterpretCast(string expr, IAstNode node, AggregateType srcType, AggregateType dstType) + { + if (srcType == dstType) + { + return expr; + } + + if (srcType == AggregateType.FP32) + { + switch (dstType) + { + case AggregateType.Bool: + return $"(as_type({expr}) != 0)"; + case AggregateType.S32: + return $"as_type({expr})"; + case AggregateType.U32: + return $"as_type({expr})"; + } + } + else if (dstType == AggregateType.FP32) + { + switch (srcType) + { + case AggregateType.Bool: + return $"as_type({ReinterpretBoolToInt(expr, node, AggregateType.S32)})"; + case AggregateType.S32: + return $"as_type({expr})"; + case AggregateType.U32: + return $"as_type({expr})"; + } + } + else if (srcType == AggregateType.Bool) + { + return ReinterpretBoolToInt(expr, node, dstType); + } + else if (dstType == AggregateType.Bool) + { + expr = InstGenHelper.Enclose(expr, node, Instruction.CompareNotEqual, isLhs: true); + + return $"({expr} != 0)"; + } + else if (dstType == AggregateType.S32) + { + return $"int({expr})"; + } + else if (dstType == AggregateType.U32) + { + return $"uint({expr})"; + } + + throw new ArgumentException($"Invalid reinterpret cast from \"{srcType}\" to \"{dstType}\"."); + } + + private static string ReinterpretBoolToInt(string expr, IAstNode node, AggregateType dstType) + { + string trueExpr = NumberFormatter.FormatInt(IrConsts.True, dstType); + string falseExpr = NumberFormatter.FormatInt(IrConsts.False, dstType); + + expr = InstGenHelper.Enclose(expr, node, Instruction.ConditionalSelect, isLhs: false); + + return $"({expr} ? {trueExpr} : {falseExpr})"; + } + } +} diff --git a/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj b/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj index cfb188daf..8b05d8829 100644 --- a/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj +++ b/src/Ryujinx.Graphics.Shader/Ryujinx.Graphics.Shader.csproj @@ -15,4 +15,11 @@ + + + + + + + diff --git a/src/Ryujinx.Graphics.Shader/SamplerType.cs b/src/Ryujinx.Graphics.Shader/SamplerType.cs index a693495fa..18285cd70 100644 --- a/src/Ryujinx.Graphics.Shader/SamplerType.cs +++ b/src/Ryujinx.Graphics.Shader/SamplerType.cs @@ -155,5 +155,51 @@ namespace Ryujinx.Graphics.Shader return typeName; } + + public static string ToMslTextureType(this SamplerType type, AggregateType aggregateType, bool image = false) + { + string typeName; + + if ((type & SamplerType.Shadow) != 0) + { + typeName = (type & SamplerType.Mask) switch + { + SamplerType.Texture2D => "depth2d", + SamplerType.TextureCube => "depthcube", + _ => throw new ArgumentException($"Invalid shadow texture type \"{type}\"."), + }; + } + else + { + typeName = (type & SamplerType.Mask) switch + { + SamplerType.Texture1D => "texture1d", + SamplerType.TextureBuffer => "texture_buffer", + SamplerType.Texture2D => "texture2d", + SamplerType.Texture3D => "texture3d", + SamplerType.TextureCube => "texturecube", + _ => throw new ArgumentException($"Invalid texture type \"{type}\"."), + }; + } + + if ((type & SamplerType.Multisample) != 0) + { + typeName += "_ms"; + } + + if ((type & SamplerType.Array) != 0) + { + typeName += "_array"; + } + + var format = aggregateType switch + { + AggregateType.S32 => "int", + AggregateType.U32 => "uint", + _ => "float" + }; + + return $"{typeName}<{format}{(image ? ", access::read_write" : "")}>"; + } } } diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/HelperFunctionsMask.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/HelperFunctionsMask.cs index 2a3d65e75..b70def78c 100644 --- a/src/Ryujinx.Graphics.Shader/StructuredIr/HelperFunctionsMask.cs +++ b/src/Ryujinx.Graphics.Shader/StructuredIr/HelperFunctionsMask.cs @@ -7,7 +7,14 @@ namespace Ryujinx.Graphics.Shader.StructuredIr { MultiplyHighS32 = 1 << 2, MultiplyHighU32 = 1 << 3, + + FindLSB = 1 << 5, + FindMSBS32 = 1 << 6, + FindMSBU32 = 1 << 7, + SwizzleAdd = 1 << 10, FSI = 1 << 11, + + Precise = 1 << 13 } } diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs index 88053658d..a1aef7f97 100644 --- a/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs +++ b/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgram.cs @@ -18,9 +18,10 @@ namespace Ryujinx.Graphics.Shader.StructuredIr ShaderDefinitions definitions, ResourceManager resourceManager, TargetLanguage targetLanguage, + bool precise, bool debugMode) { - StructuredProgramContext context = new(attributeUsage, definitions, resourceManager, debugMode); + StructuredProgramContext context = new(attributeUsage, definitions, resourceManager, precise, debugMode); for (int funcIndex = 0; funcIndex < functions.Count; funcIndex++) { @@ -321,8 +322,9 @@ namespace Ryujinx.Graphics.Shader.StructuredIr } // Those instructions needs to be emulated by using helper functions, - // because they are NVIDIA specific. Those flags helps the backend to - // decide which helper functions are needed on the final generated code. + // because they are NVIDIA specific or because the target language has + // no direct equivalent. Those flags helps the backend to decide which + // helper functions are needed on the final generated code. switch (operation.Inst) { case Instruction.MultiplyHighS32: @@ -331,6 +333,15 @@ namespace Ryujinx.Graphics.Shader.StructuredIr case Instruction.MultiplyHighU32: context.Info.HelperFunctionsMask |= HelperFunctionsMask.MultiplyHighU32; break; + case Instruction.FindLSB: + context.Info.HelperFunctionsMask |= HelperFunctionsMask.FindLSB; + break; + case Instruction.FindMSBS32: + context.Info.HelperFunctionsMask |= HelperFunctionsMask.FindMSBS32; + break; + case Instruction.FindMSBU32: + context.Info.HelperFunctionsMask |= HelperFunctionsMask.FindMSBU32; + break; case Instruction.SwizzleAdd: context.Info.HelperFunctionsMask |= HelperFunctionsMask.SwizzleAdd; break; diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramContext.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramContext.cs index 045662a1e..c26086c72 100644 --- a/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramContext.cs +++ b/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramContext.cs @@ -36,9 +36,10 @@ namespace Ryujinx.Graphics.Shader.StructuredIr AttributeUsage attributeUsage, ShaderDefinitions definitions, ResourceManager resourceManager, + bool precise, bool debugMode) { - Info = new StructuredProgramInfo(); + Info = new StructuredProgramInfo(precise); Definitions = definitions; ResourceManager = resourceManager; diff --git a/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramInfo.cs b/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramInfo.cs index ded2f2a89..585497ed3 100644 --- a/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramInfo.cs +++ b/src/Ryujinx.Graphics.Shader/StructuredIr/StructuredProgramInfo.cs @@ -10,11 +10,16 @@ namespace Ryujinx.Graphics.Shader.StructuredIr public HelperFunctionsMask HelperFunctionsMask { get; set; } - public StructuredProgramInfo() + public StructuredProgramInfo(bool precise) { Functions = new List(); IoDefinitions = new HashSet(); + + if (precise) + { + HelperFunctionsMask |= HelperFunctionsMask.Precise; + } } } } diff --git a/src/Ryujinx.Graphics.Shader/Translation/FeatureFlags.cs b/src/Ryujinx.Graphics.Shader/Translation/FeatureFlags.cs index 82a54db83..26c924e89 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/FeatureFlags.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/FeatureFlags.cs @@ -26,5 +26,6 @@ namespace Ryujinx.Graphics.Shader.Translation SharedMemory = 1 << 11, Store = 1 << 12, VtgAsCompute = 1 << 13, + Precise = 1 << 14, } } diff --git a/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs b/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs index 94691a5b4..83e4dc0ac 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/ResourceManager.cs @@ -43,6 +43,11 @@ namespace Ryujinx.Graphics.Shader.Translation private readonly Dictionary _usedTextures; private readonly Dictionary _usedImages; + private readonly List _vacConstantBuffers; + private readonly List _vacStorageBuffers; + private readonly List _vacTextures; + private readonly List _vacImages; + public int LocalMemoryId { get; private set; } public int SharedMemoryId { get; private set; } @@ -78,6 +83,11 @@ namespace Ryujinx.Graphics.Shader.Translation _usedTextures = new(); _usedImages = new(); + _vacConstantBuffers = new(); + _vacStorageBuffers = new(); + _vacTextures = new(); + _vacImages = new(); + Properties.AddOrUpdateConstantBuffer(new(BufferLayout.Std140, 0, SupportBuffer.Binding, "support_buffer", SupportBuffer.GetStructureType())); LocalMemoryId = -1; @@ -563,6 +573,75 @@ namespace Ryujinx.Graphics.Shader.Translation return descriptors.ToArray(); } + public ShaderProgramInfo GetVertexAsComputeInfo(bool isVertex = false) + { + var cbDescriptors = new BufferDescriptor[_vacConstantBuffers.Count]; + int cbDescriptorIndex = 0; + + foreach (BufferDefinition definition in _vacConstantBuffers) + { + cbDescriptors[cbDescriptorIndex++] = new BufferDescriptor(definition.Set, definition.Binding, 0, 0, 0, BufferUsageFlags.None); + } + + var sbDescriptors = new BufferDescriptor[_vacStorageBuffers.Count]; + int sbDescriptorIndex = 0; + + foreach (BufferDefinition definition in _vacStorageBuffers) + { + sbDescriptors[sbDescriptorIndex++] = new BufferDescriptor(definition.Set, definition.Binding, 0, 0, 0, BufferUsageFlags.Write); + } + + var tDescriptors = new TextureDescriptor[_vacTextures.Count]; + int tDescriptorIndex = 0; + + foreach (TextureDefinition definition in _vacTextures) + { + tDescriptors[tDescriptorIndex++] = new TextureDescriptor( + definition.Set, + definition.Binding, + definition.Type, + definition.Format, + 0, + 0, + definition.ArrayLength, + definition.Separate, + definition.Flags); + } + + var iDescriptors = new TextureDescriptor[_vacImages.Count]; + int iDescriptorIndex = 0; + + foreach (TextureDefinition definition in _vacImages) + { + iDescriptors[iDescriptorIndex++] = new TextureDescriptor( + definition.Set, + definition.Binding, + definition.Type, + definition.Format, + 0, + 0, + definition.ArrayLength, + definition.Separate, + definition.Flags); + } + + return new ShaderProgramInfo( + cbDescriptors, + sbDescriptors, + tDescriptors, + iDescriptors, + isVertex ? ShaderStage.Vertex : ShaderStage.Compute, + 0, + 0, + 0, + false, + false, + false, + false, + 0, + 0); + } + public bool TryGetCbufSlotAndHandleForTexture(int binding, out int cbufSlot, out int handle) { foreach ((TextureInfo info, TextureMeta meta) in _usedTextures) @@ -629,6 +708,30 @@ namespace Ryujinx.Graphics.Shader.Translation Properties.AddOrUpdateStorageBuffer(new(BufferLayout.Std430, setIndex, binding, name, type)); } + public void AddVertexAsComputeConstantBuffer(BufferDefinition definition) + { + _vacConstantBuffers.Add(definition); + Properties.AddOrUpdateConstantBuffer(definition); + } + + public void AddVertexAsComputeStorageBuffer(BufferDefinition definition) + { + _vacStorageBuffers.Add(definition); + Properties.AddOrUpdateStorageBuffer(definition); + } + + public void AddVertexAsComputeTexture(TextureDefinition definition) + { + _vacTextures.Add(definition); + Properties.AddOrUpdateTexture(definition); + } + + public void AddVertexAsComputeImage(TextureDefinition definition) + { + _vacImages.Add(definition); + Properties.AddOrUpdateImage(definition); + } + public static string GetShaderStagePrefix(ShaderStage stage) { uint index = (uint)stage; diff --git a/src/Ryujinx.Graphics.Shader/Translation/TargetApi.cs b/src/Ryujinx.Graphics.Shader/Translation/TargetApi.cs index 519600937..66ed3dd45 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/TargetApi.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/TargetApi.cs @@ -4,5 +4,6 @@ namespace Ryujinx.Graphics.Shader.Translation { OpenGL, Vulkan, + Metal } } diff --git a/src/Ryujinx.Graphics.Shader/Translation/TargetLanguage.cs b/src/Ryujinx.Graphics.Shader/Translation/TargetLanguage.cs index 863c7447b..9d58cb926 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/TargetLanguage.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/TargetLanguage.cs @@ -4,6 +4,6 @@ namespace Ryujinx.Graphics.Shader.Translation { Glsl, Spirv, - Arb, + Msl } } diff --git a/src/Ryujinx.Graphics.Shader/Translation/Transforms/ForcePreciseEnable.cs b/src/Ryujinx.Graphics.Shader/Translation/Transforms/ForcePreciseEnable.cs index 6b7e1410f..c774816a3 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/Transforms/ForcePreciseEnable.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/Transforms/ForcePreciseEnable.cs @@ -27,6 +27,8 @@ namespace Ryujinx.Graphics.Shader.Translation.Transforms addOp.Inst == (Instruction.FP32 | Instruction.Add) && addOp.GetSource(1).Type == OperandType.Constant) { + context.UsedFeatures |= FeatureFlags.Precise; + addOp.ForcePrecise = true; } diff --git a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs index a579433f9..bec20bc2c 100644 --- a/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs +++ b/src/Ryujinx.Graphics.Shader/Translation/TranslatorContext.cs @@ -1,5 +1,6 @@ using Ryujinx.Graphics.Shader.CodeGen; using Ryujinx.Graphics.Shader.CodeGen.Glsl; +using Ryujinx.Graphics.Shader.CodeGen.Msl; using Ryujinx.Graphics.Shader.CodeGen.Spirv; using Ryujinx.Graphics.Shader.Decoders; using Ryujinx.Graphics.Shader.IntermediateRepresentation; @@ -331,6 +332,7 @@ namespace Ryujinx.Graphics.Shader.Translation definitions, resourceManager, Options.TargetLanguage, + usedFeatures.HasFlag(FeatureFlags.Precise), Options.Flags.HasFlag(TranslationFlags.DebugMode)); int geometryVerticesPerPrimitive = Definitions.OutputTopology switch @@ -373,6 +375,7 @@ namespace Ryujinx.Graphics.Shader.Translation { TargetLanguage.Glsl => new ShaderProgram(info, TargetLanguage.Glsl, GlslGenerator.Generate(sInfo, parameters)), TargetLanguage.Spirv => new ShaderProgram(info, TargetLanguage.Spirv, SpirvGenerator.Generate(sInfo, parameters)), + TargetLanguage.Msl => new ShaderProgram(info, TargetLanguage.Msl, MslGenerator.Generate(sInfo, parameters)), _ => throw new NotImplementedException(Options.TargetLanguage.ToString()), }; } @@ -392,7 +395,7 @@ namespace Ryujinx.Graphics.Shader.Translation { int binding = resourceManager.Reservations.GetTfeBufferStorageBufferBinding(i); BufferDefinition tfeDataBuffer = new(BufferLayout.Std430, 1, binding, $"tfe_data{i}", tfeDataStruct); - resourceManager.Properties.AddOrUpdateStorageBuffer(tfeDataBuffer); + resourceManager.AddVertexAsComputeStorageBuffer(tfeDataBuffer); } } @@ -400,7 +403,7 @@ namespace Ryujinx.Graphics.Shader.Translation { int vertexInfoCbBinding = resourceManager.Reservations.VertexInfoConstantBufferBinding; BufferDefinition vertexInfoBuffer = new(BufferLayout.Std140, 0, vertexInfoCbBinding, "vb_info", VertexInfoBuffer.GetStructureType()); - resourceManager.Properties.AddOrUpdateConstantBuffer(vertexInfoBuffer); + resourceManager.AddVertexAsComputeConstantBuffer(vertexInfoBuffer); StructureType vertexOutputStruct = new(new StructureField[] { @@ -409,13 +412,13 @@ namespace Ryujinx.Graphics.Shader.Translation int vertexOutputSbBinding = resourceManager.Reservations.VertexOutputStorageBufferBinding; BufferDefinition vertexOutputBuffer = new(BufferLayout.Std430, 1, vertexOutputSbBinding, "vertex_output", vertexOutputStruct); - resourceManager.Properties.AddOrUpdateStorageBuffer(vertexOutputBuffer); + resourceManager.AddVertexAsComputeStorageBuffer(vertexOutputBuffer); if (Stage == ShaderStage.Vertex) { SetBindingPair ibSetAndBinding = resourceManager.Reservations.GetIndexBufferTextureSetAndBinding(); TextureDefinition indexBuffer = new(ibSetAndBinding.SetIndex, ibSetAndBinding.Binding, "ib_data", SamplerType.TextureBuffer); - resourceManager.Properties.AddOrUpdateTexture(indexBuffer); + resourceManager.AddVertexAsComputeTexture(indexBuffer); int inputMap = _program.AttributeUsage.UsedInputAttributes; @@ -424,7 +427,7 @@ namespace Ryujinx.Graphics.Shader.Translation int location = BitOperations.TrailingZeroCount(inputMap); SetBindingPair setAndBinding = resourceManager.Reservations.GetVertexBufferTextureSetAndBinding(location); TextureDefinition vaBuffer = new(setAndBinding.SetIndex, setAndBinding.Binding, $"vb_data{location}", SamplerType.TextureBuffer); - resourceManager.Properties.AddOrUpdateTexture(vaBuffer); + resourceManager.AddVertexAsComputeTexture(vaBuffer); inputMap &= ~(1 << location); } @@ -433,11 +436,11 @@ namespace Ryujinx.Graphics.Shader.Translation { SetBindingPair trbSetAndBinding = resourceManager.Reservations.GetTopologyRemapBufferTextureSetAndBinding(); TextureDefinition remapBuffer = new(trbSetAndBinding.SetIndex, trbSetAndBinding.Binding, "trb_data", SamplerType.TextureBuffer); - resourceManager.Properties.AddOrUpdateTexture(remapBuffer); + resourceManager.AddVertexAsComputeTexture(remapBuffer); int geometryVbOutputSbBinding = resourceManager.Reservations.GeometryVertexOutputStorageBufferBinding; BufferDefinition geometryVbOutputBuffer = new(BufferLayout.Std430, 1, geometryVbOutputSbBinding, "geometry_vb_output", vertexOutputStruct); - resourceManager.Properties.AddOrUpdateStorageBuffer(geometryVbOutputBuffer); + resourceManager.AddVertexAsComputeStorageBuffer(geometryVbOutputBuffer); StructureType geometryIbOutputStruct = new(new StructureField[] { @@ -446,7 +449,7 @@ namespace Ryujinx.Graphics.Shader.Translation int geometryIbOutputSbBinding = resourceManager.Reservations.GeometryIndexOutputStorageBufferBinding; BufferDefinition geometryIbOutputBuffer = new(BufferLayout.Std430, 1, geometryIbOutputSbBinding, "geometry_ib_output", geometryIbOutputStruct); - resourceManager.Properties.AddOrUpdateStorageBuffer(geometryIbOutputBuffer); + resourceManager.AddVertexAsComputeStorageBuffer(geometryIbOutputBuffer); } resourceManager.SetVertexAsComputeLocalMemories(Definitions.Stage, Definitions.InputTopology); @@ -479,12 +482,17 @@ namespace Ryujinx.Graphics.Shader.Translation return new ResourceReservations(GpuAccessor, IsTransformFeedbackEmulated, vertexAsCompute: true, _vertexOutput, ioUsage); } + public ShaderProgramInfo GetVertexAsComputeInfo() + { + return CreateResourceManager(true).GetVertexAsComputeInfo(); + } + public void SetVertexOutputMapForGeometryAsCompute(TranslatorContext vertexContext) { _vertexOutput = vertexContext._program.GetIoUsage(); } - public ShaderProgram GenerateVertexPassthroughForCompute() + public (ShaderProgram, ShaderProgramInfo) GenerateVertexPassthroughForCompute() { var attributeUsage = new AttributeUsage(GpuAccessor); var resourceManager = new ResourceManager(ShaderStage.Vertex, GpuAccessor); @@ -496,7 +504,7 @@ namespace Ryujinx.Graphics.Shader.Translation if (Stage == ShaderStage.Vertex) { BufferDefinition vertexInfoBuffer = new(BufferLayout.Std140, 0, vertexInfoCbBinding, "vb_info", VertexInfoBuffer.GetStructureType()); - resourceManager.Properties.AddOrUpdateConstantBuffer(vertexInfoBuffer); + resourceManager.AddVertexAsComputeConstantBuffer(vertexInfoBuffer); } StructureType vertexInputStruct = new(new StructureField[] @@ -506,7 +514,7 @@ namespace Ryujinx.Graphics.Shader.Translation int vertexDataSbBinding = reservations.VertexOutputStorageBufferBinding; BufferDefinition vertexOutputBuffer = new(BufferLayout.Std430, 1, vertexDataSbBinding, "vb_input", vertexInputStruct); - resourceManager.Properties.AddOrUpdateStorageBuffer(vertexOutputBuffer); + resourceManager.AddVertexAsComputeStorageBuffer(vertexOutputBuffer); var context = new EmitterContext(); @@ -564,14 +572,14 @@ namespace Ryujinx.Graphics.Shader.Translation LastInVertexPipeline = true }; - return Generate( + return (Generate( new[] { function }, attributeUsage, definitions, definitions, resourceManager, FeatureFlags.None, - 0); + 0), resourceManager.GetVertexAsComputeInfo(isVertex: true)); } public ShaderProgram GenerateGeometryPassthrough() diff --git a/src/Ryujinx.Headless.SDL2/Metal/MetalWindow.cs b/src/Ryujinx.Headless.SDL2/Metal/MetalWindow.cs new file mode 100644 index 000000000..5140d639b --- /dev/null +++ b/src/Ryujinx.Headless.SDL2/Metal/MetalWindow.cs @@ -0,0 +1,47 @@ +using Ryujinx.Common.Configuration; +using Ryujinx.Input.HLE; +using Ryujinx.SDL2.Common; +using SharpMetal.QuartzCore; +using System.Runtime.Versioning; +using static SDL2.SDL; + +namespace Ryujinx.Headless.SDL2.Metal +{ + [SupportedOSPlatform("macos")] + class MetalWindow : WindowBase + { + private CAMetalLayer _caMetalLayer; + + public CAMetalLayer GetLayer() + { + return _caMetalLayer; + } + + public MetalWindow( + InputManager inputManager, + GraphicsDebugLevel glLogLevel, + AspectRatio aspectRatio, + bool enableMouse, + HideCursorMode hideCursorMode, + bool ignoreControllerApplet) + : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode, ignoreControllerApplet) { } + + public override SDL_WindowFlags GetWindowFlags() => SDL_WindowFlags.SDL_WINDOW_METAL; + + protected override void InitializeWindowRenderer() + { + void CreateLayer() + { + _caMetalLayer = new CAMetalLayer(SDL_Metal_GetLayer(SDL_Metal_CreateView(WindowHandle))); + } + + SDL2Driver.MainThreadDispatcher?.Invoke(CreateLayer); + } + + protected override void InitializeRenderer() { } + + protected override void FinalizeWindowRenderer() { } + + protected override void SwapBuffers() { } + } +} diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs index 12158176a..ea789095e 100644 --- a/src/Ryujinx.Headless.SDL2/Program.cs +++ b/src/Ryujinx.Headless.SDL2/Program.cs @@ -18,9 +18,11 @@ using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.GAL.Multithreading; using Ryujinx.Graphics.Gpu; using Ryujinx.Graphics.Gpu.Shader; +using Ryujinx.Graphics.Metal; using Ryujinx.Graphics.OpenGL; using Ryujinx.Graphics.Vulkan; using Ryujinx.Graphics.Vulkan.MoltenVK; +using Ryujinx.Headless.SDL2.Metal; using Ryujinx.Headless.SDL2.OpenGL; using Ryujinx.Headless.SDL2.Vulkan; using Ryujinx.HLE; @@ -510,9 +512,14 @@ namespace Ryujinx.Headless.SDL2 private static WindowBase CreateWindow(Options options) { - return options.GraphicsBackend == GraphicsBackend.Vulkan - ? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode, options.IgnoreControllerApplet) - : new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode, options.IgnoreControllerApplet); + return options.GraphicsBackend switch + { + GraphicsBackend.Vulkan => new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode, options.IgnoreControllerApplet), + GraphicsBackend.Metal => OperatingSystem.IsMacOS() ? + new MetalWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableKeyboard, options.HideCursorMode, options.IgnoreControllerApplet) : + throw new Exception("Attempted to use Metal renderer on non-macOS platform!"), + _ => new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode, options.IgnoreControllerApplet) + }; } private static IRenderer CreateRenderer(Options options, WindowBase window) @@ -544,6 +551,11 @@ namespace Ryujinx.Headless.SDL2 preferredGpuId); } + if (options.GraphicsBackend == GraphicsBackend.Metal && window is MetalWindow metalWindow && OperatingSystem.IsMacOS()) + { + return new MetalRenderer(metalWindow.GetLayer); + } + return new OpenGLRenderer(); } diff --git a/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj index fe535e6d5..d39f2b481 100644 --- a/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj +++ b/src/Ryujinx.Headless.SDL2/Ryujinx.Headless.SDL2.csproj @@ -29,6 +29,7 @@ + diff --git a/src/Ryujinx.ShaderTools/Program.cs b/src/Ryujinx.ShaderTools/Program.cs index a84d7b466..564960c6f 100644 --- a/src/Ryujinx.ShaderTools/Program.cs +++ b/src/Ryujinx.ShaderTools/Program.cs @@ -116,7 +116,7 @@ namespace Ryujinx.ShaderTools if (options.VertexPassthrough) { - program = translatorContext.GenerateVertexPassthroughForCompute(); + (program, _) = translatorContext.GenerateVertexPassthroughForCompute(); } else { diff --git a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs index 04ddd442f..90bdc3409 100644 --- a/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs +++ b/src/Ryujinx.UI.Common/Configuration/ConfigurationState.cs @@ -305,14 +305,15 @@ namespace Ryujinx.UI.Common.Configuration private static GraphicsBackend DefaultGraphicsBackend() { - // Any system running macOS or returning any amount of valid Vulkan devices should default to Vulkan. - // Checks for if the Vulkan version and featureset is compatible should be performed within VulkanRenderer. - if (OperatingSystem.IsMacOS() || VulkanRenderer.GetPhysicalDevices().Length > 0) - { - return GraphicsBackend.Vulkan; - } + // Any system running macOS should default to auto, so it uses Vulkan everywhere and Metal in games where it works well. + if (OperatingSystem.IsMacOS()) + return GraphicsBackend.Auto; - return GraphicsBackend.OpenGl; - } - } + // Any system returning any amount of valid Vulkan devices should default to Vulkan. + // Checks for if the Vulkan version and featureset is compatible should be performed within VulkanRenderer. + return VulkanRenderer.GetPhysicalDevices().Length > 0 + ? GraphicsBackend.Vulkan + : GraphicsBackend.OpenGl; } + } +} diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index 5872b278f..abcc357a6 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -3,6 +3,7 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input; using Avalonia.Threading; +using Gommon; using LibHac.Common; using LibHac.Ns; using LibHac.Tools.FsSystem; @@ -28,6 +29,7 @@ using Ryujinx.Common.Utilities; using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.GAL.Multithreading; using Ryujinx.Graphics.Gpu; +using Ryujinx.Graphics.Metal; using Ryujinx.Graphics.OpenGL; using Ryujinx.Graphics.Vulkan; using Ryujinx.HLE; @@ -141,6 +143,23 @@ namespace Ryujinx.Ava public ulong ApplicationId { get; private set; } public bool ScreenshotRequested { get; set; } + public bool ShouldInitMetal + { + get + { + return OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64 && + ( + ( + ( + ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Auto && + RendererHost.KnownGreatMetalTitles.ContainsIgnoreCase(ApplicationId.ToString("X16")) + ) || + ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Metal + ) + ); + } + } + public AppHost( RendererHost renderer, InputManager inputManager, @@ -893,12 +912,27 @@ namespace Ryujinx.Ava VirtualFileSystem.ReloadKeySet(); // Initialize Renderer. - IRenderer renderer = ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.OpenGl - ? new OpenGLRenderer() - : VulkanRenderer.Create( + IRenderer renderer; + GraphicsBackend backend = ConfigurationState.Instance.Graphics.GraphicsBackend; + + if (ShouldInitMetal) + { +#pragma warning disable CA1416 This call site is reachable on all platforms + // The condition does a check for Mac, on top of checking if it's an ARM Mac. This isn't a problem. + renderer = new MetalRenderer((RendererHost.EmbeddedWindow as EmbeddedWindowMetal)!.CreateSurface); +#pragma warning restore CA1416 + } + else if (backend == GraphicsBackend.Vulkan || (backend == GraphicsBackend.Auto && !ShouldInitMetal)) + { + renderer = VulkanRenderer.Create( ConfigurationState.Instance.Graphics.PreferredGpu, (RendererHost.EmbeddedWindow as EmbeddedWindowVulkan)!.CreateSurface, VulkanHelper.GetRequiredInstanceExtensions); + } + else + { + renderer = new OpenGLRenderer(); + } BackendThreading threadingMode = ConfigurationState.Instance.Graphics.BackendThreading; @@ -1111,10 +1145,11 @@ namespace Ryujinx.Ava public void InitStatus() { - _viewModel.BackendText = ConfigurationState.Instance.Graphics.GraphicsBackend.Value switch + _viewModel.BackendText = RendererHost.Backend switch { GraphicsBackend.Vulkan => "Vulkan", GraphicsBackend.OpenGl => "OpenGL", + GraphicsBackend.Metal => "Metal", _ => throw new NotImplementedException() }; diff --git a/src/Ryujinx/Assets/locales.json b/src/Ryujinx/Assets/locales.json index 2e86df0aa..cdf43f474 100644 --- a/src/Ryujinx/Assets/locales.json +++ b/src/Ryujinx/Assets/locales.json @@ -19677,6 +19677,54 @@ "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": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "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": "", + "he_IL": "", + "it_IT": "", + "ja_JP": "", + "ko_KR": "", + "no_NO": "", + "pl_PL": "", + "pt_BR": "", + "ru_RU": "", + "th_TH": "", + "tr_TR": "", + "uk_UA": "", + "zh_CN": "", + "zh_TW": "" + } + }, { "ID": "SettingsEnableTextureRecompression", "Translations": { diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index 05fd66b90..3c24b8e27 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -198,6 +198,7 @@ namespace Ryujinx.Ava { "opengl" => GraphicsBackend.OpenGl, "vulkan" => GraphicsBackend.Vulkan, + "metal" => GraphicsBackend.Metal, _ => ConfigurationState.Instance.Graphics.GraphicsBackend }; diff --git a/src/Ryujinx/Ryujinx.csproj b/src/Ryujinx/Ryujinx.csproj index ce75b1d87..d5bad2ee6 100644 --- a/src/Ryujinx/Ryujinx.csproj +++ b/src/Ryujinx/Ryujinx.csproj @@ -66,6 +66,7 @@ + diff --git a/src/Ryujinx/UI/Renderer/EmbeddedWindowMetal.cs b/src/Ryujinx/UI/Renderer/EmbeddedWindowMetal.cs new file mode 100644 index 000000000..eaf6f7bdf --- /dev/null +++ b/src/Ryujinx/UI/Renderer/EmbeddedWindowMetal.cs @@ -0,0 +1,20 @@ +using SharpMetal.QuartzCore; +using System; + +namespace Ryujinx.Ava.UI.Renderer +{ + public class EmbeddedWindowMetal : EmbeddedWindow + { + public CAMetalLayer CreateSurface() + { + if (OperatingSystem.IsMacOS()) + { + return new CAMetalLayer(MetalLayer); + } + else + { + throw new NotSupportedException(); + } + } + } +} diff --git a/src/Ryujinx/UI/Renderer/RendererHost.axaml.cs b/src/Ryujinx/UI/Renderer/RendererHost.axaml.cs index 4bf10d0d7..d191a11ed 100644 --- a/src/Ryujinx/UI/Renderer/RendererHost.axaml.cs +++ b/src/Ryujinx/UI/Renderer/RendererHost.axaml.cs @@ -1,8 +1,10 @@ using Avalonia; using Avalonia.Controls; +using Gommon; using Ryujinx.Common.Configuration; using Ryujinx.UI.Common.Configuration; using System; +using System.Runtime.InteropServices; namespace Ryujinx.Ava.UI.Renderer { @@ -17,18 +19,64 @@ namespace Ryujinx.Ava.UI.Renderer { InitializeComponent(); - if (ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.OpenGl) + EmbeddedWindow = ConfigurationState.Instance.Graphics.GraphicsBackend.Value switch { - EmbeddedWindow = new EmbeddedWindowOpenGL(); - } - else - { - EmbeddedWindow = new EmbeddedWindowVulkan(); - } + GraphicsBackend.OpenGl => new EmbeddedWindowOpenGL(), + GraphicsBackend.Metal => new EmbeddedWindowMetal(), + GraphicsBackend.Vulkan or GraphicsBackend.Auto => new EmbeddedWindowVulkan(), + _ => throw new NotSupportedException() + }; Initialize(); } + public static readonly string[] KnownGreatMetalTitles = + [ + "01006A800016E000", // Smash Ultimate + "0100000000010000", // Super Mario Odyessy + "01008C0016544000", // Sea of Stars + "01005CA01580E000", // Persona 5 + "010028600EBDA000", // Mario 3D World + ]; + + public GraphicsBackend Backend => + EmbeddedWindow switch + { + EmbeddedWindowVulkan => GraphicsBackend.Vulkan, + EmbeddedWindowOpenGL => GraphicsBackend.OpenGl, + EmbeddedWindowMetal => GraphicsBackend.Metal, + _ => throw new NotImplementedException() + }; + + public RendererHost(string titleId) + { + InitializeComponent(); + + switch (ConfigurationState.Instance.Graphics.GraphicsBackend.Value) + { + case GraphicsBackend.Auto: + EmbeddedWindow = + OperatingSystem.IsMacOS() && + RuntimeInformation.ProcessArchitecture == Architecture.Arm64 && + KnownGreatMetalTitles.ContainsIgnoreCase(titleId) + ? new EmbeddedWindowMetal() + : new EmbeddedWindowVulkan(); + break; + case GraphicsBackend.OpenGl: + EmbeddedWindow = new EmbeddedWindowOpenGL(); + break; + case GraphicsBackend.Metal: + EmbeddedWindow = new EmbeddedWindowMetal(); + break; + case GraphicsBackend.Vulkan: + EmbeddedWindow = new EmbeddedWindowVulkan(); + break; + } + + Initialize(); + } + + private void Initialize() { EmbeddedWindow.WindowCreated += CurrentWindow_WindowCreated; diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index ae373c267..f7cd83ed6 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -1938,7 +1938,7 @@ namespace Ryujinx.Ava.UI.ViewModels PrepareLoadScreen(); - RendererHostControl = new RendererHost(); + RendererHostControl = new RendererHost(application.Id.ToString("X16")); AppHost = new AppHost( RendererHostControl, diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 0824e3f86..85ba203f9 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -122,7 +122,7 @@ namespace Ryujinx.Ava.UI.ViewModels public bool IsOpenGLAvailable => !OperatingSystem.IsMacOS(); - public bool IsHypervisorAvailable => OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64; + public bool IsAppleSiliconMac => OperatingSystem.IsMacOS() && RuntimeInformation.ProcessArchitecture == Architecture.Arm64; public bool GameDirectoryChanged { @@ -252,7 +252,8 @@ namespace Ryujinx.Ava.UI.ViewModels public bool IsCustomResolutionScaleActive => _resolutionScale == 4; public bool IsScalingFilterActive => _scalingFilter == (int)Ryujinx.Common.Configuration.ScalingFilter.Fsr; - public bool IsVulkanSelected => GraphicsBackendIndex == 0; + public bool IsVulkanSelected => + GraphicsBackendIndex == 1 || (GraphicsBackendIndex == 0 && !OperatingSystem.IsMacOS()); public bool UseHypervisor { get; set; } public bool DisableP2P { get; set; } @@ -432,7 +433,7 @@ namespace Ryujinx.Ava.UI.ViewModels if (devices.Length == 0) { IsVulkanAvailable = false; - GraphicsBackendIndex = 1; + GraphicsBackendIndex = 2; } else { diff --git a/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml index c7f03a45d..83f908a9c 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsCPUView.axaml @@ -69,7 +69,7 @@ diff --git a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml index 219efcef8..a2559f393 100644 --- a/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml +++ b/src/Ryujinx/UI/Views/Settings/SettingsGraphicsView.axaml @@ -33,16 +33,24 @@ ToolTip.Tip="{ext:Locale SettingsTabGraphicsBackendTooltip}" Text="{ext:Locale SettingsTabGraphicsBackend}" Width="250" /> - + + + + + + + diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs b/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs index d8c88bed8..b004d9fba 100644 --- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs @@ -56,34 +56,34 @@ namespace Ryujinx.Ava.UI.Windows { switch (navItem.Tag.ToString()) { - case "UiPage": + case nameof(UiPage): UiPage.ViewModel = ViewModel; NavPanel.Content = UiPage; break; - case "InputPage": + case nameof(InputPage): NavPanel.Content = InputPage; break; - case "HotkeysPage": + case nameof(HotkeysPage): NavPanel.Content = HotkeysPage; break; - case "SystemPage": + case nameof(SystemPage): SystemPage.ViewModel = ViewModel; NavPanel.Content = SystemPage; break; - case "CpuPage": + case nameof(CpuPage): NavPanel.Content = CpuPage; break; - case "GraphicsPage": + case nameof(GraphicsPage): NavPanel.Content = GraphicsPage; break; - case "AudioPage": + case nameof(AudioPage): NavPanel.Content = AudioPage; break; - case "NetworkPage": + case nameof(NetworkPage): NetworkPage.ViewModel = ViewModel; NavPanel.Content = NetworkPage; break; - case "LoggingPage": + case nameof(LoggingPage): NavPanel.Content = LoggingPage; break; default: From 3cb996bf5c56b53eff6c4c43b2d5a8492863c90b Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 24 Dec 2024 01:15:23 -0600 Subject: [PATCH 66/79] UI: Log backend used when Auto is selected --- src/Ryujinx/UI/Renderer/RendererHost.axaml.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Ryujinx/UI/Renderer/RendererHost.axaml.cs b/src/Ryujinx/UI/Renderer/RendererHost.axaml.cs index d191a11ed..072c0ba4d 100644 --- a/src/Ryujinx/UI/Renderer/RendererHost.axaml.cs +++ b/src/Ryujinx/UI/Renderer/RendererHost.axaml.cs @@ -2,6 +2,7 @@ using Avalonia; using Avalonia.Controls; using Gommon; using Ryujinx.Common.Configuration; +using Ryujinx.Common.Logging; using Ryujinx.UI.Common.Configuration; using System; using System.Runtime.InteropServices; @@ -61,6 +62,11 @@ namespace Ryujinx.Ava.UI.Renderer KnownGreatMetalTitles.ContainsIgnoreCase(titleId) ? new EmbeddedWindowMetal() : new EmbeddedWindowVulkan(); + + string backendText = EmbeddedWindow is EmbeddedWindowVulkan ? "Vulkan" : "Metal"; + + Logger.Info?.Print(LogClass.Gpu, $"Auto: Using {backendText}"); + break; case GraphicsBackend.OpenGl: EmbeddedWindow = new EmbeddedWindowOpenGL(); From 2f540dc88cff3709f7e46ad6224dddf6ce9e0056 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 24 Dec 2024 01:23:01 -0600 Subject: [PATCH 67/79] infra: chore: fix/silence compile warnings --- src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs | 1 + src/Ryujinx.Graphics.Metal/EncoderStateManager.cs | 1 + src/Ryujinx.Graphics.Metal/MetalRenderer.cs | 5 ++++- .../CodeGen/Msl/Instructions/InstGen.cs | 1 + src/Ryujinx/AppHost.cs | 2 +- 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs index 4473e8cb3..0924c60f8 100644 --- a/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs +++ b/src/Ryujinx.Graphics.Gpu/Shader/ShaderCache.cs @@ -838,6 +838,7 @@ namespace Ryujinx.Graphics.Gpu.Shader TargetApi.OpenGL => TargetLanguage.Glsl, TargetApi.Vulkan => GraphicsConfig.EnableSpirvCompilationOnVulkan ? TargetLanguage.Spirv : TargetLanguage.Glsl, TargetApi.Metal => TargetLanguage.Msl, + _ => throw new NotImplementedException() }; return new TranslationOptions(lang, api, flags); diff --git a/src/Ryujinx.Graphics.Metal/EncoderStateManager.cs b/src/Ryujinx.Graphics.Metal/EncoderStateManager.cs index 0093ac148..169e0142d 100644 --- a/src/Ryujinx.Graphics.Metal/EncoderStateManager.cs +++ b/src/Ryujinx.Graphics.Metal/EncoderStateManager.cs @@ -1767,6 +1767,7 @@ namespace Ryujinx.Graphics.Metal Constants.StorageBuffersSetIndex => Constants.StorageBuffersIndex, Constants.TexturesSetIndex => Constants.TexturesIndex, Constants.ImagesSetIndex => Constants.ImagesIndex, + _ => throw new NotImplementedException() }; } diff --git a/src/Ryujinx.Graphics.Metal/MetalRenderer.cs b/src/Ryujinx.Graphics.Metal/MetalRenderer.cs index 935a5b3b1..7afd30886 100644 --- a/src/Ryujinx.Graphics.Metal/MetalRenderer.cs +++ b/src/Ryujinx.Graphics.Metal/MetalRenderer.cs @@ -21,9 +21,12 @@ namespace Ryujinx.Graphics.Metal private Pipeline _pipeline; private Window _window; - public uint ProgramCount { get; set; } = 0; + public uint ProgramCount { get; set; } +#pragma warning disable CS0067 // The event is never used public event EventHandler ScreenCaptured; +#pragma warning restore CS0067 + public bool PreferThreading => true; public IPipeline Pipeline => _pipeline; public IWindow Window => _window; diff --git a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGen.cs b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGen.cs index 715688987..57177e402 100644 --- a/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGen.cs +++ b/src/Ryujinx.Graphics.Shader/CodeGen/Msl/Instructions/InstGen.cs @@ -125,6 +125,7 @@ namespace Ryujinx.Graphics.Shader.CodeGen.Msl.Instructions Instruction.Add => "PreciseFAdd", Instruction.Subtract => "PreciseFSub", Instruction.Multiply => "PreciseFMul", + _ => throw new NotImplementedException() }; return $"{func}({expr[0]}, {expr[1]})"; diff --git a/src/Ryujinx/AppHost.cs b/src/Ryujinx/AppHost.cs index abcc357a6..86b5eafa1 100644 --- a/src/Ryujinx/AppHost.cs +++ b/src/Ryujinx/AppHost.cs @@ -917,7 +917,7 @@ namespace Ryujinx.Ava if (ShouldInitMetal) { -#pragma warning disable CA1416 This call site is reachable on all platforms +#pragma warning disable CA1416 // This call site is reachable on all platforms // The condition does a check for Mac, on top of checking if it's an ARM Mac. This isn't a problem. renderer = new MetalRenderer((RendererHost.EmbeddedWindow as EmbeddedWindowMetal)!.CreateSurface); #pragma warning restore CA1416 From ff667a5c84240985e1d5d9a726a9f8d603299763 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 24 Dec 2024 01:35:27 -0600 Subject: [PATCH 68/79] chore: Fix .ctor message source --- src/Ryujinx/UI/Renderer/RendererHost.axaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Ryujinx/UI/Renderer/RendererHost.axaml.cs b/src/Ryujinx/UI/Renderer/RendererHost.axaml.cs index 072c0ba4d..7bc7e9160 100644 --- a/src/Ryujinx/UI/Renderer/RendererHost.axaml.cs +++ b/src/Ryujinx/UI/Renderer/RendererHost.axaml.cs @@ -65,7 +65,7 @@ namespace Ryujinx.Ava.UI.Renderer string backendText = EmbeddedWindow is EmbeddedWindowVulkan ? "Vulkan" : "Metal"; - Logger.Info?.Print(LogClass.Gpu, $"Auto: Using {backendText}"); + Logger.Info?.PrintMsg(LogClass.Gpu, $"Auto: Using {backendText}"); break; case GraphicsBackend.OpenGl: From b05eab21a2707371a2608c71faa60171a1e93789 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 24 Dec 2024 01:40:29 -0600 Subject: [PATCH 69/79] misc: always log backend when creating embeddedwindow --- src/Ryujinx/UI/Renderer/RendererHost.axaml.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Ryujinx/UI/Renderer/RendererHost.axaml.cs b/src/Ryujinx/UI/Renderer/RendererHost.axaml.cs index 7bc7e9160..c66b266c0 100644 --- a/src/Ryujinx/UI/Renderer/RendererHost.axaml.cs +++ b/src/Ryujinx/UI/Renderer/RendererHost.axaml.cs @@ -62,11 +62,6 @@ namespace Ryujinx.Ava.UI.Renderer KnownGreatMetalTitles.ContainsIgnoreCase(titleId) ? new EmbeddedWindowMetal() : new EmbeddedWindowVulkan(); - - string backendText = EmbeddedWindow is EmbeddedWindowVulkan ? "Vulkan" : "Metal"; - - Logger.Info?.PrintMsg(LogClass.Gpu, $"Auto: Using {backendText}"); - break; case GraphicsBackend.OpenGl: EmbeddedWindow = new EmbeddedWindowOpenGL(); @@ -78,6 +73,16 @@ namespace Ryujinx.Ava.UI.Renderer EmbeddedWindow = new EmbeddedWindowVulkan(); break; } + + string backendText = EmbeddedWindow switch + { + EmbeddedWindowVulkan => "Vulkan", + EmbeddedWindowOpenGL => "OpenGL", + EmbeddedWindowMetal => "Metal", + _ => throw new NotImplementedException() + }; + + Logger.Info?.PrintMsg(LogClass.Gpu, $"Backend ({ConfigurationState.Instance.Graphics.GraphicsBackend.Value}): {backendText}"); Initialize(); } From 4d7350fc6e512f22d80261b5014fd759d3e5ce74 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 24 Dec 2024 13:23:43 -0600 Subject: [PATCH 70/79] UI: Copy Title ID by clicking on it. --- src/Ryujinx/App.axaml.cs | 3 +++ .../UI/Controls/ApplicationListView.axaml | 20 ++++++++++++---- .../UI/Controls/ApplicationListView.axaml.cs | 23 +++++++++++++++++++ src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 3 +++ 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/Ryujinx/App.axaml.cs b/src/Ryujinx/App.axaml.cs index 15ada201c..9c1170d08 100644 --- a/src/Ryujinx/App.axaml.cs +++ b/src/Ryujinx/App.axaml.cs @@ -32,6 +32,9 @@ namespace Ryujinx.Ava .ApplicationLifetime.Cast() .MainWindow.Cast(); + public static IClassicDesktopStyleApplicationLifetime DesktopLifetime => Current! + .ApplicationLifetime.Cast(); + public static void SetTaskbarProgress(TaskBarProgressBarState state) => MainWindow.PlatformFeatures.SetTaskBarProgressBarState(state); public static void SetTaskbarProgressValue(ulong current, ulong total) => MainWindow.PlatformFeatures.SetTaskBarProgressBarValue(current, total); public static void SetTaskbarProgressValue(long current, long total) => SetTaskbarProgressValue(Convert.ToUInt64(current), Convert.ToUInt64(total)); diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml b/src/Ryujinx/UI/Controls/ApplicationListView.axaml index 8a72ebfbf..90b657ee0 100644 --- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml @@ -101,11 +101,21 @@ VerticalAlignment="Top" Orientation="Vertical" Spacing="5"> - + it.IdString == idText.Text); + if (appData is null) + return; + + await clipboard.SetTextAsync(appData.IdString); + + NotificationHelper.Show("Copied Title ID", $"{appData.Name} ({appData.IdString})", NotificationType.Information); + } + } } } diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index c433d7fdb..88e82c89e 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -98,6 +98,9 @@ namespace Ryujinx.Ava.UI.Windows StatusBarHeight = StatusBarView.StatusBar.MinHeight; MenuBarHeight = MenuBar.MinHeight; + ApplicationList.DataContext = DataContext; + ApplicationGrid.DataContext = DataContext; + SetWindowSizePosition(); if (Program.PreviewerDetached) From 16a60fdf123c54e5f98d887e8ceb5352c3c5b18f Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 24 Dec 2024 13:39:48 -0600 Subject: [PATCH 71/79] UI: Rename App to RyujinxApp Add more NotificationHelper methods Simplify ID copy logic --- src/Ryujinx/Common/ApplicationHelper.cs | 9 ++-- src/Ryujinx/Program.cs | 6 +-- src/Ryujinx/{App.axaml => RyujinxApp.axaml} | 2 +- .../{App.axaml.cs => RyujinxApp.axaml.cs} | 12 +++-- .../UI/Controls/ApplicationListView.axaml.cs | 20 +++++---- src/Ryujinx/UI/Helpers/NotificationHelper.cs | 45 +++++++++++++++++-- .../UI/ViewModels/AboutWindowViewModel.cs | 4 +- .../UI/ViewModels/MainWindowViewModel.cs | 2 +- src/Ryujinx/UI/Windows/AmiiboWindow.axaml.cs | 4 +- src/Ryujinx/UI/Windows/CheatWindow.axaml.cs | 4 +- src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 4 +- .../UI/Windows/SettingsWindow.axaml.cs | 2 +- src/Ryujinx/Updater.cs | 8 ++-- 13 files changed, 82 insertions(+), 40 deletions(-) rename src/Ryujinx/{App.axaml => RyujinxApp.axaml} (94%) rename src/Ryujinx/{App.axaml.cs => RyujinxApp.axaml.cs} (94%) diff --git a/src/Ryujinx/Common/ApplicationHelper.cs b/src/Ryujinx/Common/ApplicationHelper.cs index db5961347..1c6b53dee 100644 --- a/src/Ryujinx/Common/ApplicationHelper.cs +++ b/src/Ryujinx/Common/ApplicationHelper.cs @@ -146,7 +146,7 @@ namespace Ryujinx.Ava.Common var cancellationToken = new CancellationTokenSource(); UpdateWaitWindow waitingDialog = new( - App.FormatTitle(LocaleKeys.DialogNcaExtractionTitle), + RyujinxApp.FormatTitle(LocaleKeys.DialogNcaExtractionTitle), LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.DialogNcaExtractionMessage, ncaSectionType, Path.GetFileName(titleFilePath)), cancellationToken); @@ -268,10 +268,9 @@ namespace Ryujinx.Ava.Common { Dispatcher.UIThread.Post(waitingDialog.Close); - NotificationHelper.Show( - App.FormatTitle(LocaleKeys.DialogNcaExtractionTitle), - $"{titleName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}", - NotificationType.Information); + NotificationHelper.ShowInformation( + RyujinxApp.FormatTitle(LocaleKeys.DialogNcaExtractionTitle), + $"{titleName}\n\n{LocaleManager.Instance[LocaleKeys.DialogNcaExtractionSuccessMessage]}"); } } diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs index 3c24b8e27..2ec60ac70 100644 --- a/src/Ryujinx/Program.cs +++ b/src/Ryujinx/Program.cs @@ -65,7 +65,7 @@ namespace Ryujinx.Ava } public static AppBuilder BuildAvaloniaApp() => - AppBuilder.Configure() + AppBuilder.Configure() .UsePlatformDetect() .With(new X11PlatformOptions { @@ -100,7 +100,7 @@ namespace Ryujinx.Ava // Delete backup files after updating. Task.Run(Updater.CleanupUpdate); - Console.Title = $"{App.FullAppName} Console {Version}"; + Console.Title = $"{RyujinxApp.FullAppName} Console {Version}"; // Hook unhandled exception and process exit events. AppDomain.CurrentDomain.UnhandledException += (sender, e) @@ -225,7 +225,7 @@ namespace Ryujinx.Ava private static void PrintSystemInfo() { - Logger.Notice.Print(LogClass.Application, $"{App.FullAppName} Version: {Version}"); + Logger.Notice.Print(LogClass.Application, $"{RyujinxApp.FullAppName} Version: {Version}"); SystemInfo.Gather().Print(); var enabledLogLevels = Logger.GetEnabledLevels().ToArray(); diff --git a/src/Ryujinx/App.axaml b/src/Ryujinx/RyujinxApp.axaml similarity index 94% rename from src/Ryujinx/App.axaml rename to src/Ryujinx/RyujinxApp.axaml index 5c96f97f2..e07d7ff26 100644 --- a/src/Ryujinx/App.axaml +++ b/src/Ryujinx/RyujinxApp.axaml @@ -1,5 +1,5 @@ diff --git a/src/Ryujinx/App.axaml.cs b/src/Ryujinx/RyujinxApp.axaml.cs similarity index 94% rename from src/Ryujinx/App.axaml.cs rename to src/Ryujinx/RyujinxApp.axaml.cs index 9c1170d08..bbef20aa0 100644 --- a/src/Ryujinx/App.axaml.cs +++ b/src/Ryujinx/RyujinxApp.axaml.cs @@ -1,5 +1,6 @@ using Avalonia; using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Input.Platform; using Avalonia.Markup.Xaml; using Avalonia.Platform; using Avalonia.Styling; @@ -19,7 +20,7 @@ using System.Diagnostics; namespace Ryujinx.Ava { - public class App : Application + public class RyujinxApp : Application { internal static string FormatTitle(LocaleKeys? windowTitleKey = null) => windowTitleKey is null @@ -32,8 +33,11 @@ namespace Ryujinx.Ava .ApplicationLifetime.Cast() .MainWindow.Cast(); - public static IClassicDesktopStyleApplicationLifetime DesktopLifetime => Current! - .ApplicationLifetime.Cast(); + public static bool IsClipboardAvailable(out IClipboard clipboard) + { + clipboard = MainWindow.Clipboard; + return clipboard != null; + } public static void SetTaskbarProgress(TaskBarProgressBarState state) => MainWindow.PlatformFeatures.SetTaskBarProgressBarState(state); public static void SetTaskbarProgressValue(ulong current, ulong total) => MainWindow.PlatformFeatures.SetTaskBarProgressBarValue(current, total); @@ -135,7 +139,7 @@ namespace Ryujinx.Ava }; public static ThemeVariant DetectSystemTheme() => - Current is App { PlatformSettings: not null } app + Current is RyujinxApp { PlatformSettings: not null } app ? ConvertThemeVariant(app.PlatformSettings.GetColorValues().ThemeVariant) : ThemeVariant.Default; } diff --git a/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs b/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs index 6cdff3463..5b0730d5a 100644 --- a/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs +++ b/src/Ryujinx/UI/Controls/ApplicationListView.axaml.cs @@ -43,17 +43,19 @@ namespace Ryujinx.Ava.UI.Controls if (sender is not Button { Content: TextBlock idText }) return; + + if (!RyujinxApp.IsClipboardAvailable(out var clipboard)) + return; - if (App.MainWindow.Clipboard is { } clipboard) - { - var appData = mwvm.Applications.FirstOrDefault(it => it.IdString == idText.Text); - if (appData is null) - return; + var appData = mwvm.Applications.FirstOrDefault(it => it.IdString == idText.Text); + if (appData is null) + return; + + await clipboard.SetTextAsync(appData.IdString); - await clipboard.SetTextAsync(appData.IdString); - - NotificationHelper.Show("Copied Title ID", $"{appData.Name} ({appData.IdString})", NotificationType.Information); - } + NotificationHelper.ShowInformation( + "Copied Title ID", + $"{appData.Name} ({appData.IdString})"); } } } diff --git a/src/Ryujinx/UI/Helpers/NotificationHelper.cs b/src/Ryujinx/UI/Helpers/NotificationHelper.cs index 656a8b52f..74029a4b1 100644 --- a/src/Ryujinx/UI/Helpers/NotificationHelper.cs +++ b/src/Ryujinx/UI/Helpers/NotificationHelper.cs @@ -62,9 +62,46 @@ namespace Ryujinx.Ava.UI.Helpers _notifications.Add(new Notification(title, text, type, delay, onClick, onClose)); } - public static void ShowError(string message) - { - Show(LocaleManager.Instance[LocaleKeys.DialogErrorTitle], $"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}", NotificationType.Error); - } + public static void ShowError(string message) => + ShowError( + LocaleManager.Instance[LocaleKeys.DialogErrorTitle], + $"{LocaleManager.Instance[LocaleKeys.DialogErrorMessage]}\n\n{message}" + ); + + public static void ShowInformation(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) => + Show( + title, + text, + NotificationType.Information, + waitingExit, + onClick, + onClose); + + public static void ShowSuccess(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) => + Show( + title, + text, + NotificationType.Success, + waitingExit, + onClick, + onClose); + + public static void ShowWarning(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) => + Show( + title, + text, + NotificationType.Warning, + waitingExit, + onClick, + onClose); + + public static void ShowError(string title, string text, bool waitingExit = false, Action onClick = null, Action onClose = null) => + Show( + title, + text, + NotificationType.Error, + waitingExit, + onClick, + onClose); } } diff --git a/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs index 23d0f963c..607bff792 100644 --- a/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/AboutWindowViewModel.cs @@ -51,7 +51,7 @@ namespace Ryujinx.Ava.UI.ViewModels public AboutWindowViewModel() { - Version = App.FullAppName + "\n" + Program.Version; + Version = RyujinxApp.FullAppName + "\n" + Program.Version; UpdateLogoTheme(ConfigurationState.Instance.UI.BaseStyle.Value); ThemeManager.ThemeChanged += ThemeManager_ThemeChanged; @@ -64,7 +64,7 @@ namespace Ryujinx.Ava.UI.ViewModels private void UpdateLogoTheme(string theme) { - bool isDarkTheme = theme == "Dark" || (theme == "Auto" && App.DetectSystemTheme() == ThemeVariant.Dark); + bool isDarkTheme = theme == "Dark" || (theme == "Auto" && RyujinxApp.DetectSystemTheme() == ThemeVariant.Dark); string basePath = "resm:Ryujinx.UI.Common.Resources."; string themeSuffix = isDarkTheme ? "Dark.png" : "Light.png"; diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index f7cd83ed6..3ff05785a 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -2051,7 +2051,7 @@ namespace Ryujinx.Ava.UI.ViewModels Dispatcher.UIThread.InvokeAsync(() => { - Title = App.FormatTitle(); + Title = RyujinxApp.FormatTitle(); }); } diff --git a/src/Ryujinx/UI/Windows/AmiiboWindow.axaml.cs b/src/Ryujinx/UI/Windows/AmiiboWindow.axaml.cs index 6182e6b50..9a940c938 100644 --- a/src/Ryujinx/UI/Windows/AmiiboWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/AmiiboWindow.axaml.cs @@ -16,7 +16,7 @@ namespace Ryujinx.Ava.UI.Windows InitializeComponent(); - Title = App.FormatTitle(LocaleKeys.Amiibo); + Title = RyujinxApp.FormatTitle(LocaleKeys.Amiibo); } public AmiiboWindow() @@ -27,7 +27,7 @@ namespace Ryujinx.Ava.UI.Windows if (Program.PreviewerDetached) { - Title = App.FormatTitle(LocaleKeys.Amiibo); + Title = RyujinxApp.FormatTitle(LocaleKeys.Amiibo); } } diff --git a/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs b/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs index 8c8d56b34..2fc9617fb 100644 --- a/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/CheatWindow.axaml.cs @@ -28,7 +28,7 @@ namespace Ryujinx.Ava.UI.Windows InitializeComponent(); - Title = App.FormatTitle(LocaleKeys.CheatWindowTitle); + Title = RyujinxApp.FormatTitle(LocaleKeys.CheatWindowTitle); } public CheatWindow(VirtualFileSystem virtualFileSystem, string titleId, string titleName, string titlePath) @@ -93,7 +93,7 @@ namespace Ryujinx.Ava.UI.Windows DataContext = this; - Title = App.FormatTitle(LocaleKeys.CheatWindowTitle); + Title = RyujinxApp.FormatTitle(LocaleKeys.CheatWindowTitle); } public void Save() diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index 88e82c89e..e621b42ec 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -86,7 +86,7 @@ namespace Ryujinx.Ava.UI.Windows UiHandler = new AvaHostUIHandler(this); - ViewModel.Title = App.FormatTitle(); + ViewModel.Title = RyujinxApp.FormatTitle(); TitleBar.ExtendsContentIntoTitleBar = !ConfigurationState.Instance.ShowTitleBar; TitleBar.TitleBarHitTestType = (ConfigurationState.Instance.ShowTitleBar) ? TitleBarHitTestType.Simple : TitleBarHitTestType.Complex; @@ -117,7 +117,7 @@ namespace Ryujinx.Ava.UI.Windows /// private static void OnPlatformColorValuesChanged(object sender, PlatformColorValues e) { - if (Application.Current is App app) + if (Application.Current is RyujinxApp app) app.ApplyConfiguredTheme(ConfigurationState.Instance.UI.BaseStyle); } diff --git a/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs b/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs index b004d9fba..0c0345107 100644 --- a/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/SettingsWindow.axaml.cs @@ -14,7 +14,7 @@ namespace Ryujinx.Ava.UI.Windows public SettingsWindow(VirtualFileSystem virtualFileSystem, ContentManager contentManager) { - Title = App.FormatTitle(LocaleKeys.Settings); + Title = RyujinxApp.FormatTitle(LocaleKeys.Settings); DataContext = ViewModel = new SettingsViewModel(virtualFileSystem, contentManager); diff --git a/src/Ryujinx/Updater.cs b/src/Ryujinx/Updater.cs index 21d991d97..3d4f11317 100644 --- a/src/Ryujinx/Updater.cs +++ b/src/Ryujinx/Updater.cs @@ -76,7 +76,7 @@ namespace Ryujinx.Ava if (!Version.TryParse(Program.Version, out Version currentVersion)) { - Logger.Error?.Print(LogClass.Application, $"Failed to convert the current {App.FullAppName} version!"); + Logger.Error?.Print(LogClass.Application, $"Failed to convert the current {RyujinxApp.FullAppName} version!"); await ContentDialogHelper.CreateWarningDialog( LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedMessage], @@ -159,7 +159,7 @@ namespace Ryujinx.Ava if (!Version.TryParse(_buildVer, out Version newVersion)) { - Logger.Error?.Print(LogClass.Application, $"Failed to convert the received {App.FullAppName} version from GitHub!"); + Logger.Error?.Print(LogClass.Application, $"Failed to convert the received {RyujinxApp.FullAppName} version from GitHub!"); await ContentDialogHelper.CreateWarningDialog( LocaleManager.Instance[LocaleKeys.DialogUpdaterConvertFailedGithubMessage], @@ -266,7 +266,7 @@ namespace Ryujinx.Ava SubHeader = LocaleManager.Instance[LocaleKeys.UpdaterDownloading], IconSource = new SymbolIconSource { Symbol = Symbol.Download }, ShowProgressBar = true, - XamlRoot = App.MainWindow, + XamlRoot = RyujinxApp.MainWindow, }; taskDialog.Opened += (s, e) => @@ -490,7 +490,7 @@ namespace Ryujinx.Ava bytesWritten += readSize; taskDialog.SetProgressBarState(GetPercentage(bytesWritten, totalBytes), TaskDialogProgressState.Normal); - App.SetTaskbarProgressValue(bytesWritten, totalBytes); + RyujinxApp.SetTaskbarProgressValue(bytesWritten, totalBytes); updateFileStream.Write(buffer, 0, readSize); } From a0a4f78cff86d44edee680edf3a7ecc1059ebd9c Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 24 Dec 2024 20:47:14 -0600 Subject: [PATCH 72/79] UI: Thin down the borders of the app icon a little bit and trim down the file size significantly. --- src/Ryujinx.HLE/FileSystem/ContentManager.cs | 26 ++++++++++-------- .../Resources/Logo_Ryujinx_AntiAlias.png | Bin 0 -> 12236 bytes .../Resources/Logo_Thiccjinx.png | Bin 623701 -> 0 bytes .../Ryujinx.UI.Common.csproj | 2 +- .../UI/ViewModels/MainWindowViewModel.cs | 2 +- .../UI/Views/Main/MainMenuBarView.axaml | 2 +- 6 files changed, 17 insertions(+), 15 deletions(-) create mode 100644 src/Ryujinx.UI.Common/Resources/Logo_Ryujinx_AntiAlias.png delete mode 100644 src/Ryujinx.UI.Common/Resources/Logo_Thiccjinx.png diff --git a/src/Ryujinx.HLE/FileSystem/ContentManager.cs b/src/Ryujinx.HLE/FileSystem/ContentManager.cs index 9bda759a5..ec0f58b01 100644 --- a/src/Ryujinx.HLE/FileSystem/ContentManager.cs +++ b/src/Ryujinx.HLE/FileSystem/ContentManager.cs @@ -1034,16 +1034,16 @@ namespace Ryujinx.HLE.FileSystem switch (fileName) { case "prod.keys": - verified = verifyKeys(lines, genericPattern); + verified = VerifyKeys(lines, genericPattern); break; case "title.keys": - verified = verifyKeys(lines, titlePattern); + verified = VerifyKeys(lines, titlePattern); break; case "console.keys": - verified = verifyKeys(lines, genericPattern); + verified = VerifyKeys(lines, genericPattern); break; case "dev.keys": - verified = verifyKeys(lines, genericPattern); + verified = VerifyKeys(lines, genericPattern); break; default: throw new FormatException($"Keys file name \"{fileName}\" not supported. Only \"prod.keys\", \"title.keys\", \"console.keys\", \"dev.keys\" are supported."); @@ -1056,20 +1056,22 @@ namespace Ryujinx.HLE.FileSystem { throw new FileNotFoundException($"Keys file not found at \"{filePath}\"."); } - } - private bool verifyKeys(string[] lines, string regex) - { - foreach (string line in lines) + return; + + bool VerifyKeys(string[] lines, string regex) { - if (!Regex.IsMatch(line, regex)) + foreach (string line in lines) { - return false; + if (!Regex.IsMatch(line, regex)) + { + return false; + } } + return true; } - return true; } - + public bool AreKeysAlredyPresent(string pathToCheck) { string[] fileNames = { "prod.keys", "title.keys", "console.keys", "dev.keys" }; diff --git a/src/Ryujinx.UI.Common/Resources/Logo_Ryujinx_AntiAlias.png b/src/Ryujinx.UI.Common/Resources/Logo_Ryujinx_AntiAlias.png new file mode 100644 index 0000000000000000000000000000000000000000..a00c7ce68ca8571bef818609284190f2b133159b GIT binary patch literal 12236 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 z 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

$i`fKCNV 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 - + diff --git a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs index 3ff05785a..04881b58d 100644 --- a/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/MainWindowViewModel.cs @@ -131,7 +131,7 @@ namespace Ryujinx.Ava.UI.ViewModels // For an example of this, download canary 1.2.95, then open the settings menu, and look at the icon in the top-left. // The border gets reduced to colored pixels in the 4 corners. public static readonly Bitmap IconBitmap = - new(Assembly.GetAssembly(typeof(ConfigurationState))!.GetManifestResourceStream("Ryujinx.UI.Common.Resources.Logo_Thiccjinx.png")!); + new(Assembly.GetAssembly(typeof(ConfigurationState))!.GetManifestResourceStream("Ryujinx.UI.Common.Resources.Logo_Ryujinx_AntiAlias.png")!); public MainWindow Window { get; init; } diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml index 7d8135dcf..8207f8e03 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml @@ -18,7 +18,7 @@ Height="25" Width="25" ToolTip.Tip="{Binding Title}" - Source="resm:Ryujinx.UI.Common.Resources.Logo_Thiccjinx.png?assembly=Ryujinx.UI.Common" /> + Source="resm:Ryujinx.UI.Common.Resources.Logo_Ryujinx_AntiAlias.png?assembly=Ryujinx.UI.Common" /> Date: Tue, 24 Dec 2024 21:00:41 -0600 Subject: [PATCH 73/79] UI: Make custom title bar window controls extend exactly as long as the menu bar is tall --- src/Ryujinx/UI/Windows/MainWindow.axaml.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs index e621b42ec..660caa605 100644 --- a/src/Ryujinx/UI/Windows/MainWindow.axaml.cs +++ b/src/Ryujinx/UI/Windows/MainWindow.axaml.cs @@ -91,12 +91,14 @@ namespace Ryujinx.Ava.UI.Windows TitleBar.ExtendsContentIntoTitleBar = !ConfigurationState.Instance.ShowTitleBar; TitleBar.TitleBarHitTestType = (ConfigurationState.Instance.ShowTitleBar) ? TitleBarHitTestType.Simple : TitleBarHitTestType.Complex; - // Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024) - TitleBarHeight = (ConfigurationState.Instance.ShowTitleBar ? TitleBar.Height : 0); - // NOTE: Height of MenuBar and StatusBar is not usable here, since it would still be 0 at this point. StatusBarHeight = StatusBarView.StatusBar.MinHeight; MenuBarHeight = MenuBar.MinHeight; + + TitleBar.Height = MenuBarHeight; + + // Correctly size window when 'TitleBar' is enabled (Nov. 14, 2024) + TitleBarHeight = (ConfigurationState.Instance.ShowTitleBar ? TitleBar.Height : 0); ApplicationList.DataContext = DataContext; ApplicationGrid.DataContext = DataContext; From 41acc4b1f39e6586ee1462e6e42a8b9c3717809a Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 24 Dec 2024 21:14:17 -0600 Subject: [PATCH 74/79] UI: misc: Collapse repeated identical Border usages into a helper control. --- .../UI/Views/Main/MainStatusBarView.axaml | 71 +++---------------- .../UI/Views/Main/MainStatusBarView.axaml.cs | 23 ++++++ 2 files changed, 32 insertions(+), 62 deletions(-) diff --git a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml index 6e72a8b4b..ea00927d8 100644 --- a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml @@ -7,6 +7,7 @@ xmlns:ext="clr-namespace:Ryujinx.Ava.Common.Markup" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:viewModels="clr-namespace:Ryujinx.Ava.UI.ViewModels" + xmlns:local="clr-namespace:Ryujinx.Ava.UI.Views.Main" xmlns:config="clr-namespace:Ryujinx.Common.Configuration;assembly=Ryujinx.Common" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Ryujinx.Ava.UI.Views.Main.MainStatusBarView" @@ -132,14 +133,7 @@ - + - + - + - + - + - + - + - + */ + } } From f0aa7eedf633e8ba2342cc83b5da52692065f961 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 24 Dec 2024 21:15:13 -0600 Subject: [PATCH 75/79] lol --- src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs index ba391ba0a..12d5473ea 100644 --- a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml.cs @@ -85,14 +85,5 @@ namespace Ryujinx.Ava.UI.Views.Main Background = Brushes.Gray; BorderThickness = new Thickness(1); } - /* - */ } } From 0ca4d6e921d3f1ea9827d3a242c9a838c6b7ddf1 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 24 Dec 2024 21:55:12 -0600 Subject: [PATCH 76/79] misc: Move StatusBarSeparator into Controls namespace, rename to MiniVerticalSeparator add bulk property change event method give each markup extension its own property name --- src/Ryujinx/Common/LocaleManager.cs | 4 +- .../Common/Markup/BasicMarkupExtension.cs | 2 +- src/Ryujinx/Common/Markup/MarkupExtensions.cs | 3 ++ ...{SliderScroll.axaml.cs => SliderScroll.cs} | 0 src/Ryujinx/UI/Controls/StatusBarSeparator.cs | 19 +++++++ src/Ryujinx/UI/ViewModels/BaseModel.cs | 9 ++++ .../UI/ViewModels/SettingsViewModel.cs | 15 +++--- .../UI/ViewModels/XCITrimmerViewModel.cs | 50 ++++++++++--------- .../UI/Views/Main/MainStatusBarView.axaml | 16 +++--- .../UI/Views/Main/MainStatusBarView.axaml.cs | 21 +------- 10 files changed, 78 insertions(+), 61 deletions(-) rename src/Ryujinx/UI/Controls/{SliderScroll.axaml.cs => SliderScroll.cs} (100%) create mode 100644 src/Ryujinx/UI/Controls/StatusBarSeparator.cs diff --git a/src/Ryujinx/Common/LocaleManager.cs b/src/Ryujinx/Common/LocaleManager.cs index f29efb15a..270b666df 100644 --- a/src/Ryujinx/Common/LocaleManager.cs +++ b/src/Ryujinx/Common/LocaleManager.cs @@ -109,7 +109,7 @@ namespace Ryujinx.Ava.Common.Locale { _dynamicValues[key] = values; - OnPropertyChanged("Item"); + OnPropertyChanged("Translation"); return this[key]; } @@ -138,7 +138,7 @@ namespace Ryujinx.Ava.Common.Locale _localeStrings[key] = val; } - OnPropertyChanged("Item"); + OnPropertyChanged("Translation"); LocaleChanged?.Invoke(); } diff --git a/src/Ryujinx/Common/Markup/BasicMarkupExtension.cs b/src/Ryujinx/Common/Markup/BasicMarkupExtension.cs index b1b7361a6..364c08c0b 100644 --- a/src/Ryujinx/Common/Markup/BasicMarkupExtension.cs +++ b/src/Ryujinx/Common/Markup/BasicMarkupExtension.cs @@ -14,7 +14,7 @@ namespace Ryujinx.Ava.Common.Markup { internal abstract class BasicMarkupExtension : MarkupExtension { - public virtual string Name => "Item"; + public abstract string Name { get; } public virtual Action? Setter => null; protected abstract T? Value { get; } diff --git a/src/Ryujinx/Common/Markup/MarkupExtensions.cs b/src/Ryujinx/Common/Markup/MarkupExtensions.cs index cae6d8c2c..9e8ff00ef 100644 --- a/src/Ryujinx/Common/Markup/MarkupExtensions.cs +++ b/src/Ryujinx/Common/Markup/MarkupExtensions.cs @@ -6,16 +6,19 @@ namespace Ryujinx.Ava.Common.Markup { internal class IconExtension(string iconString) : BasicMarkupExtension { + public override string Name => "Icon"; protected override Icon Value => new() { Value = iconString }; } internal class SpinningIconExtension(string iconString) : BasicMarkupExtension { + public override string Name => "SIcon"; protected override Icon Value => new() { Value = iconString, Animation = IconAnimation.Spin }; } internal class LocaleExtension(LocaleKeys key) : BasicMarkupExtension { + public override string Name => "Translation"; protected override string Value => LocaleManager.Instance[key]; protected override void ConfigureBindingExtension(CompiledBindingExtension bindingExtension) diff --git a/src/Ryujinx/UI/Controls/SliderScroll.axaml.cs b/src/Ryujinx/UI/Controls/SliderScroll.cs similarity index 100% rename from src/Ryujinx/UI/Controls/SliderScroll.axaml.cs rename to src/Ryujinx/UI/Controls/SliderScroll.cs diff --git a/src/Ryujinx/UI/Controls/StatusBarSeparator.cs b/src/Ryujinx/UI/Controls/StatusBarSeparator.cs new file mode 100644 index 000000000..7888879f5 --- /dev/null +++ b/src/Ryujinx/UI/Controls/StatusBarSeparator.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; + +namespace Ryujinx.Ava.UI.Controls +{ + public class MiniVerticalSeparator : Border + { + public MiniVerticalSeparator() + { + Width = 2; + Height = 12; + Margin = new Thickness(); + BorderBrush = Brushes.Gray; + Background = Brushes.Gray; + BorderThickness = new Thickness(1); + } + } +} diff --git a/src/Ryujinx/UI/ViewModels/BaseModel.cs b/src/Ryujinx/UI/ViewModels/BaseModel.cs index 4db9cf812..d8f2e9096 100644 --- a/src/Ryujinx/UI/ViewModels/BaseModel.cs +++ b/src/Ryujinx/UI/ViewModels/BaseModel.cs @@ -1,3 +1,4 @@ +using System; using System.ComponentModel; using System.Runtime.CompilerServices; @@ -11,5 +12,13 @@ namespace Ryujinx.Ava.UI.ViewModels { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } + + protected void OnPropertiesChanged(params ReadOnlySpan propertyNames) + { + foreach (var propertyName in propertyNames) + { + OnPropertyChanged(propertyName); + } + } } } diff --git a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs index 85ba203f9..7504147b2 100644 --- a/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/SettingsViewModel.cs @@ -71,8 +71,7 @@ namespace Ryujinx.Ava.UI.ViewModels { _resolutionScale = value; - OnPropertyChanged(nameof(CustomResolutionScale)); - OnPropertyChanged(nameof(IsCustomResolutionScaleActive)); + OnPropertiesChanged(nameof(CustomResolutionScale), nameof(IsCustomResolutionScaleActive)); } } @@ -181,8 +180,9 @@ namespace Ryujinx.Ava.UI.ViewModels int newInterval = (int)((value / 100f) * 60); _customVSyncInterval = newInterval; _customVSyncIntervalPercentageProxy = value; - OnPropertyChanged((nameof(CustomVSyncInterval))); - OnPropertyChanged((nameof(CustomVSyncIntervalPercentageText))); + OnPropertiesChanged( + nameof(CustomVSyncInterval), + nameof(CustomVSyncIntervalPercentageText)); } } @@ -190,7 +190,7 @@ namespace Ryujinx.Ava.UI.ViewModels { get { - string text = CustomVSyncIntervalPercentageProxy.ToString() + "%"; + string text = CustomVSyncIntervalPercentageProxy + "%"; return text; } } @@ -221,8 +221,9 @@ namespace Ryujinx.Ava.UI.ViewModels _customVSyncInterval = value; int newPercent = (int)((value / 60f) * 100); _customVSyncIntervalPercentageProxy = newPercent; - OnPropertyChanged(nameof(CustomVSyncIntervalPercentageProxy)); - OnPropertyChanged(nameof(CustomVSyncIntervalPercentageText)); + OnPropertiesChanged( + nameof(CustomVSyncIntervalPercentageProxy), + nameof(CustomVSyncIntervalPercentageText)); OnPropertyChanged(); } } diff --git a/src/Ryujinx/UI/ViewModels/XCITrimmerViewModel.cs b/src/Ryujinx/UI/ViewModels/XCITrimmerViewModel.cs index e1f9eead5..1bca27330 100644 --- a/src/Ryujinx/UI/ViewModels/XCITrimmerViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/XCITrimmerViewModel.cs @@ -91,39 +91,42 @@ namespace Ryujinx.Ava.UI.ViewModels private void SortingChanged() { - OnPropertyChanged(nameof(IsSortedByName)); - OnPropertyChanged(nameof(IsSortedBySaved)); - OnPropertyChanged(nameof(SortingAscending)); - OnPropertyChanged(nameof(SortingField)); - OnPropertyChanged(nameof(SortingFieldName)); + OnPropertiesChanged( + nameof(IsSortedByName), + nameof(IsSortedBySaved), + nameof(SortingAscending), + nameof(SortingField), + nameof(SortingFieldName)); + SortAndFilter(); } private void DisplayedChanged() { - OnPropertyChanged(nameof(Status)); - OnPropertyChanged(nameof(DisplayedXCIFiles)); - OnPropertyChanged(nameof(SelectedDisplayedXCIFiles)); + OnPropertiesChanged(nameof(Status), nameof(DisplayedXCIFiles), nameof(SelectedDisplayedXCIFiles)); } private void ApplicationsChanged() { - OnPropertyChanged(nameof(AllXCIFiles)); - OnPropertyChanged(nameof(Status)); - OnPropertyChanged(nameof(PotentialSavings)); - OnPropertyChanged(nameof(ActualSavings)); - OnPropertyChanged(nameof(CanTrim)); - OnPropertyChanged(nameof(CanUntrim)); + OnPropertiesChanged( + nameof(AllXCIFiles), + nameof(Status), + nameof(PotentialSavings), + nameof(ActualSavings), + nameof(CanTrim), + nameof(CanUntrim)); + DisplayedChanged(); SortAndFilter(); } private void SelectionChanged(bool displayedChanged = true) { - OnPropertyChanged(nameof(Status)); - OnPropertyChanged(nameof(CanTrim)); - OnPropertyChanged(nameof(CanUntrim)); - OnPropertyChanged(nameof(SelectedXCIFiles)); + OnPropertiesChanged( + nameof(Status), + nameof(CanTrim), + nameof(CanUntrim), + nameof(SelectedXCIFiles)); if (displayedChanged) OnPropertyChanged(nameof(SelectedDisplayedXCIFiles)); @@ -131,11 +134,12 @@ namespace Ryujinx.Ava.UI.ViewModels private void ProcessingChanged() { - OnPropertyChanged(nameof(Processing)); - OnPropertyChanged(nameof(Cancel)); - OnPropertyChanged(nameof(Status)); - OnPropertyChanged(nameof(CanTrim)); - OnPropertyChanged(nameof(CanUntrim)); + OnPropertiesChanged( + nameof(Processing), + nameof(Cancel), + nameof(Status), + nameof(CanTrim), + nameof(CanUntrim)); } private IEnumerable GetSelectedDisplayedXCIFiles() diff --git a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml index ea00927d8..6871fd3f9 100644 --- a/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml +++ b/src/Ryujinx/UI/Views/Main/MainStatusBarView.axaml @@ -133,7 +133,7 @@ - + - + - + - + - + - + - + - + 0, - > 1 => 1, - _ => newValue, - }; + Window.ViewModel.Volume = Math.Clamp(newValue, 0, 1); e.Handled = true; } } - - public class StatusBarSeparator : Border - { - public StatusBarSeparator() - { - Width = 2; - Height = 12; - Margin = new Thickness(); - BorderBrush = Brushes.Gray; - Background = Brushes.Gray; - BorderThickness = new Thickness(1); - } - } } From 0bacdb8765988d2d9aff3a859d9c6cb64cc5e142 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Tue, 24 Dec 2024 22:19:58 -0600 Subject: [PATCH 77/79] Improve locale file parsing error descriptions --- src/Ryujinx/Common/LocaleManager.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Ryujinx/Common/LocaleManager.cs b/src/Ryujinx/Common/LocaleManager.cs index 270b666df..e788a3adc 100644 --- a/src/Ryujinx/Common/LocaleManager.cs +++ b/src/Ryujinx/Common/LocaleManager.cs @@ -143,11 +143,7 @@ namespace Ryujinx.Ava.Common.Locale LocaleChanged?.Invoke(); } - #nullable enable - private static LocalesJson? _localeData; - - #nullable disable private static Dictionary LoadJsonLanguage(string languageCode) { @@ -158,18 +154,28 @@ namespace Ryujinx.Ava.Common.Locale foreach (LocalesEntry locale in _localeData.Value.Locales) { - if (locale.Translations.Count != _localeData.Value.Languages.Count) + if (locale.Translations.Count < _localeData.Value.Languages.Count) { throw new Exception($"Locale key {{{locale.ID}}} is missing languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!"); + } + + if (locale.Translations.Count > _localeData.Value.Languages.Count) + { + throw new Exception($"Locale key {{{locale.ID}}} has too many languages! Has {locale.Translations.Count} translations, expected {_localeData.Value.Languages.Count}!"); } if (!Enum.TryParse(locale.ID, out var localeKey)) continue; localeStrings[localeKey] = - locale.Translations.TryGetValue(languageCode, out string val) && val != string.Empty + locale.Translations.TryGetValue(languageCode, out string val) && !string.IsNullOrEmpty(val) ? val : locale.Translations[DefaultLanguageCode]; + + if (string.IsNullOrEmpty(localeStrings[localeKey])) + { + throw new Exception($"Locale key '{locale.ID}' has no valid translations for desired language {languageCode}! {DefaultLanguageCode} is an empty string or null"); + } } return localeStrings; From e6644626fceaed218b6a393deb7ee382aa304750 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Wed, 25 Dec 2024 00:06:29 -0600 Subject: [PATCH 78/79] UI: Fix negative space savings in XCI trimmer --- .../UI/Helpers/XCITrimmerFileSpaceSavingsConverter.cs | 5 +++-- src/Ryujinx/UI/ViewModels/XCITrimmerViewModel.cs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Ryujinx/UI/Helpers/XCITrimmerFileSpaceSavingsConverter.cs b/src/Ryujinx/UI/Helpers/XCITrimmerFileSpaceSavingsConverter.cs index c6e814e90..14e8e178a 100644 --- a/src/Ryujinx/UI/Helpers/XCITrimmerFileSpaceSavingsConverter.cs +++ b/src/Ryujinx/UI/Helpers/XCITrimmerFileSpaceSavingsConverter.cs @@ -1,6 +1,7 @@ using Avalonia; using Avalonia.Data; using Avalonia.Data.Converters; +using Gommon; using Ryujinx.Ava.Common.Locale; using Ryujinx.UI.Common.Models; using System; @@ -32,11 +33,11 @@ namespace Ryujinx.Ava.UI.Helpers if (app.CurrentSavingsB < app.PotentialSavingsB) { - return LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleXCICanSaveLabel, (app.PotentialSavingsB - app.CurrentSavingsB) / _bytesPerMB); + return LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleXCICanSaveLabel, ((app.PotentialSavingsB - app.CurrentSavingsB) / _bytesPerMB).CoerceAtLeast(0)); } else { - return LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleXCISavingLabel, app.CurrentSavingsB / _bytesPerMB); + return LocaleManager.Instance.UpdateAndGetDynamicValue(LocaleKeys.TitleXCISavingLabel, (app.CurrentSavingsB / _bytesPerMB).CoerceAtLeast(0)); } } diff --git a/src/Ryujinx/UI/ViewModels/XCITrimmerViewModel.cs b/src/Ryujinx/UI/ViewModels/XCITrimmerViewModel.cs index 1bca27330..402b182af 100644 --- a/src/Ryujinx/UI/ViewModels/XCITrimmerViewModel.cs +++ b/src/Ryujinx/UI/ViewModels/XCITrimmerViewModel.cs @@ -364,7 +364,7 @@ namespace Ryujinx.Ava.UI.ViewModels value = _processingApplication.Value with { PercentageProgress = null }; if (value.HasValue) - _displayedXCIFiles.ReplaceWith(value.Value); + _displayedXCIFiles.ReplaceWith(value); _processingApplication = value; OnPropertyChanged(); From 412d4065b89df44ddeb86e96e8e0cc54545e8d05 Mon Sep 17 00:00:00 2001 From: Evan Husted Date: Wed, 25 Dec 2024 00:56:01 -0600 Subject: [PATCH 79/79] UI: Abstract applet launch logic for future potential applets Optimize locale loading (remove always loading english, that was only needed with the old system) --- .../Helper/AppletMetadata.cs | 64 +++++++++++++++++++ src/Ryujinx/Common/LocaleManager.cs | 47 ++++---------- .../UI/Views/Main/MainMenuBarView.axaml.cs | 25 ++------ 3 files changed, 81 insertions(+), 55 deletions(-) create mode 100644 src/Ryujinx.UI.Common/Helper/AppletMetadata.cs diff --git a/src/Ryujinx.UI.Common/Helper/AppletMetadata.cs b/src/Ryujinx.UI.Common/Helper/AppletMetadata.cs new file mode 100644 index 000000000..644b7fe74 --- /dev/null +++ b/src/Ryujinx.UI.Common/Helper/AppletMetadata.cs @@ -0,0 +1,64 @@ +using LibHac.Common; +using LibHac.Ncm; +using LibHac.Ns; +using LibHac.Tools.FsSystem.NcaUtils; +using Ryujinx.HLE; +using Ryujinx.HLE.FileSystem; +using Ryujinx.UI.App.Common; + +namespace Ryujinx.UI.Common.Helper +{ + public readonly struct AppletMetadata + { + private readonly ContentManager _contentManager; + + public string Name { get; } + public ulong ProgramId { get; } + + public string Version { get; } + + public AppletMetadata(ContentManager contentManager, string name, ulong programId, string version = "1.0.0") + : this(name, programId, version) + { + _contentManager = contentManager; + } + + public AppletMetadata(string name, ulong programId, string version = "1.0.0") + { + Name = name; + ProgramId = programId; + Version = version; + } + + public string GetContentPath(ContentManager contentManager) + => (contentManager ?? _contentManager) + .GetInstalledContentPath(ProgramId, StorageId.BuiltInSystem, NcaContentType.Program); + + public bool CanStart(ContentManager contentManager, out ApplicationData appData, out BlitStruct appControl) + { + contentManager ??= _contentManager; + if (contentManager == null) + { + appData = null; + appControl = new BlitStruct(0); + return false; + } + + appData = new() + { + Name = Name, + Id = ProgramId, + Path = GetContentPath(contentManager) + }; + + if (string.IsNullOrEmpty(appData.Path)) + { + appControl = new BlitStruct(0); + return false; + } + + appControl = StructHelpers.CreateCustomNacpData(Name, Version); + return true; + } + } +} diff --git a/src/Ryujinx/Common/LocaleManager.cs b/src/Ryujinx/Common/LocaleManager.cs index e788a3adc..70b04ec95 100644 --- a/src/Ryujinx/Common/LocaleManager.cs +++ b/src/Ryujinx/Common/LocaleManager.cs @@ -1,7 +1,6 @@ using Gommon; using Ryujinx.Ava.UI.ViewModels; using Ryujinx.Common; -using Ryujinx.Common.Logging; using Ryujinx.Common.Utilities; using Ryujinx.UI.Common.Configuration; using System; @@ -17,7 +16,6 @@ namespace Ryujinx.Ava.Common.Locale private const string DefaultLanguageCode = "en_US"; private readonly Dictionary _localeStrings; - private Dictionary _localeDefaultStrings; private readonly ConcurrentDictionary _dynamicValues; private string _localeLanguageCode; @@ -27,7 +25,6 @@ namespace Ryujinx.Ava.Common.Locale public LocaleManager() { _localeStrings = new Dictionary(); - _localeDefaultStrings = new Dictionary(); _dynamicValues = new ConcurrentDictionary(); Load(); @@ -37,9 +34,7 @@ namespace Ryujinx.Ava.Common.Locale { var localeLanguageCode = !string.IsNullOrEmpty(ConfigurationState.Instance.UI.LanguageCode.Value) ? ConfigurationState.Instance.UI.LanguageCode.Value : CultureInfo.CurrentCulture.Name.Replace('-', '_'); - - // Load en_US as default, if the target language translation is missing or incomplete. - LoadDefaultLanguage(); + LoadLanguage(localeLanguageCode); // Save whatever we ended up with. @@ -66,26 +61,14 @@ namespace Ryujinx.Ava.Common.Locale } catch { - // If formatting failed use the default text instead. - if (_localeDefaultStrings.TryGetValue(key, out value)) - try - { - return string.Format(value, dynamicValue); - } - catch - { - // If formatting the default text failed return the key. - return key.ToString(); - } + // If formatting the text failed, + // continue to the below line & return the text without formatting. } return value; } - - // If the locale doesn't contain the key return the default one. - return _localeDefaultStrings.TryGetValue(key, out string defaultValue) - ? defaultValue - : key.ToString(); // If the locale text doesn't exist return the key. + + return key.ToString(); // If the locale text doesn't exist return the key. } set { @@ -114,11 +97,6 @@ namespace Ryujinx.Ava.Common.Locale return this[key]; } - private void LoadDefaultLanguage() - { - _localeDefaultStrings = LoadJsonLanguage(DefaultLanguageCode); - } - public void LoadLanguage(string languageCode) { var locale = LoadJsonLanguage(languageCode); @@ -126,7 +104,7 @@ namespace Ryujinx.Ava.Common.Locale if (locale == null) { _localeLanguageCode = DefaultLanguageCode; - locale = _localeDefaultStrings; + locale = LoadJsonLanguage(_localeLanguageCode); } else { @@ -167,15 +145,16 @@ namespace Ryujinx.Ava.Common.Locale if (!Enum.TryParse(locale.ID, out var localeKey)) continue; - localeStrings[localeKey] = - locale.Translations.TryGetValue(languageCode, out string val) && !string.IsNullOrEmpty(val) - ? val - : locale.Translations[DefaultLanguageCode]; - - if (string.IsNullOrEmpty(localeStrings[localeKey])) + var str = locale.Translations.TryGetValue(languageCode, out string val) && !string.IsNullOrEmpty(val) + ? val + : locale.Translations[DefaultLanguageCode]; + + if (string.IsNullOrEmpty(str)) { throw new Exception($"Locale key '{locale.ID}' has no valid translations for desired language {languageCode}! {DefaultLanguageCode} is an empty string or null"); } + + localeStrings[localeKey] = str; } return localeStrings; diff --git a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs index fa900be81..be8a000e7 100644 --- a/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs +++ b/src/Ryujinx/UI/Views/Main/MainMenuBarView.axaml.cs @@ -3,8 +3,6 @@ using Avalonia.Controls; using Avalonia.Interactivity; using Avalonia.Threading; using Gommon; -using LibHac.Ncm; -using LibHac.Tools.FsSystem.NcaUtils; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.UI.Helpers; using Ryujinx.Ava.UI.ViewModels; @@ -12,8 +10,6 @@ using Ryujinx.Ava.UI.Windows; using Ryujinx.Common; using Ryujinx.Common.Utilities; using Ryujinx.HLE.HOS.Services.Nfc.AmiiboDecryption; -using Ryujinx.HLE; -using Ryujinx.UI.App.Common; using Ryujinx.UI.Common; using Ryujinx.UI.Common.Configuration; using Ryujinx.UI.Common.Helper; @@ -124,26 +120,13 @@ namespace Ryujinx.Ava.UI.Views.Main ViewModel.LoadConfigurableHotKeys(); } + public static readonly AppletMetadata MiiApplet = new("miiEdit", 0x0100000000001009); + public async void OpenMiiApplet(object sender, RoutedEventArgs e) { - const string AppletName = "miiEdit"; - const ulong AppletProgramId = 0x0100000000001009; - const string AppletVersion = "1.0.0"; - - string contentPath = ViewModel.ContentManager.GetInstalledContentPath(AppletProgramId, StorageId.BuiltInSystem, NcaContentType.Program); - - if (!string.IsNullOrEmpty(contentPath)) + if (MiiApplet.CanStart(ViewModel.ContentManager, out var appData, out var nacpData)) { - ApplicationData applicationData = new() - { - Name = AppletName, - Id = AppletProgramId, - Path = contentPath - }; - - var nacpData = StructHelpers.CreateCustomNacpData(AppletName, AppletVersion); - - await ViewModel.LoadApplication(applicationData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen, nacpData); + await ViewModel.LoadApplication(appData, ViewModel.IsFullScreen || ViewModel.StartGamesInFullscreen, nacpData); } }