diff options
Diffstat (limited to 'Ryujinx.Ava')
47 files changed, 2843 insertions, 276 deletions
diff --git a/Ryujinx.Ava/AppHost.cs b/Ryujinx.Ava/AppHost.cs index 22c4ab5e..bd9c808e 100644 --- a/Ryujinx.Ava/AppHost.cs +++ b/Ryujinx.Ava/AppHost.cs @@ -1,5 +1,6 @@ using ARMeilleure.Translation; using ARMeilleure.Translation.PTC; +using Avalonia; using Avalonia.Input; using Avalonia.Threading; using LibHac.Tools.FsSystem; @@ -13,6 +14,7 @@ using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Input; using Ryujinx.Ava.Ui.Controls; using Ryujinx.Ava.Ui.Models; +using Ryujinx.Ava.Ui.Vulkan; using Ryujinx.Ava.Ui.Windows; using Ryujinx.Common; using Ryujinx.Common.Configuration; @@ -22,6 +24,7 @@ using Ryujinx.Graphics.GAL; using Ryujinx.Graphics.GAL.Multithreading; using Ryujinx.Graphics.Gpu; using Ryujinx.Graphics.OpenGL; +using Ryujinx.Graphics.Vulkan; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS; using Ryujinx.HLE.HOS.Services.Account.Acc; @@ -366,6 +369,7 @@ namespace Ryujinx.Ava ConfigurationState.Instance.System.IgnoreMissingServices.Event -= UpdateIgnoreMissingServicesState; ConfigurationState.Instance.Graphics.AspectRatio.Event -= UpdateAspectRatioState; ConfigurationState.Instance.System.EnableDockedMode.Event -= UpdateDockedModeState; + ConfigurationState.Instance.System.AudioVolume.Event -= UpdateAudioVolumeState; _gpuCancellationTokenSource.Cancel(); _gpuCancellationTokenSource.Dispose(); @@ -587,7 +591,23 @@ namespace Ryujinx.Ava { VirtualFileSystem.ReloadKeySet(); - IRenderer renderer = new Renderer(); + IRenderer renderer; + + if (Program.UseVulkan) + { + var vulkan = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>(); + renderer = new VulkanRenderer(vulkan.Instance.InternalHandle, + vulkan.Device.InternalHandle, + vulkan.PhysicalDevice.InternalHandle, + vulkan.Device.Queue.InternalHandle, + vulkan.PhysicalDevice.QueueFamilyIndex, + vulkan.Device.Lock); + } + else + { + renderer = new OpenGLRenderer(); + } + IHardwareDeviceDriver deviceDriver = new DummyHardwareDeviceDriver(); BackendThreading threadingMode = ConfigurationState.Instance.Graphics.BackendThreading; @@ -795,9 +815,12 @@ namespace Ryujinx.Ava _renderer.ScreenCaptured += Renderer_ScreenCaptured; - (_renderer as Renderer).InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext(Renderer.GameContext)); + if (!Program.UseVulkan) + { + (_renderer as OpenGLRenderer).InitializeBackgroundContext(SPBOpenGLContext.CreateBackgroundContext((Renderer as OpenGLRendererControl).GameContext)); - Renderer.MakeCurrent(); + Renderer.MakeCurrent(); + } Device.Gpu.Renderer.Initialize(_glLogLevel); @@ -856,16 +879,15 @@ namespace Ryujinx.Ava dockedMode += $" ({scale}x)"; } - string vendor = _renderer is Renderer renderer ? renderer.GpuVendor : ""; - StatusUpdatedEvent?.Invoke(this, new StatusUpdatedEventArgs( Device.EnableDeviceVsync, Device.GetVolume(), + Program.UseVulkan ? "Vulkan" : "OpenGL", dockedMode, ConfigurationState.Instance.Graphics.AspectRatio.Value.ToText(), LocaleManager.Instance["Game"] + $": {Device.Statistics.GetGameFrameRate():00.00} FPS ({Device.Statistics.GetGameFrameTime():00.00} ms)", $"FIFO: {Device.Statistics.GetFifoPercent():00.00} %", - $"GPU: {vendor}")); + $"GPU: {_renderer.GetHardwareInfo().GpuVendor}")); Renderer.Present(image); } diff --git a/Ryujinx.Ava/Assets/Locales/de_DE.json b/Ryujinx.Ava/Assets/Locales/de_DE.json index b24856fa..62b201d7 100644 --- a/Ryujinx.Ava/Assets/Locales/de_DE.json +++ b/Ryujinx.Ava/Assets/Locales/de_DE.json @@ -122,7 +122,7 @@ "SettingsTabSystemExpandDramSize": "Erweitere DRAM Größe auf 6GB", "SettingsTabSystemIgnoreMissingServices": "Ignoriere fehlende Dienste", "SettingsTabGraphics": "Grafik", - "SettingsTabGraphicsEnhancements": "Verbesserungen", + "SettingsTabGraphicsAPI": "Grafik-API", "SettingsTabGraphicsEnableShaderCache": "Aktiviere den Shader Cache", "SettingsTabGraphicsAnisotropicFiltering": "Anisotrope Filterung:", "SettingsTabGraphicsAnisotropicFilteringAuto": "Auto", @@ -416,7 +416,7 @@ "CommonFavorite": "Favoriten", "OrderAscending": "Aufsteigend", "OrderDescending": "Absteigend", - "SettingsTabGraphicsFeatures": "Erweiterungen", + "SettingsTabGraphicsFeatures": "Erweiterungen & Verbesserungen", "ErrorWindowTitle": "Fehler-Fenster", "ToggleDiscordTooltip": "Aktiviert/Deaktiviert Discord Rich Presence", "AddGameDirBoxTooltip": "Gibt das Spielverzeichnis an, das der Liste hinzuzufügt wird", diff --git a/Ryujinx.Ava/Assets/Locales/el_GR.json b/Ryujinx.Ava/Assets/Locales/el_GR.json index 187b2e45..242bcec7 100644 --- a/Ryujinx.Ava/Assets/Locales/el_GR.json +++ b/Ryujinx.Ava/Assets/Locales/el_GR.json @@ -122,7 +122,7 @@ "SettingsTabSystemExpandDramSize": "Επέκταση μεγέθους DRAM στα 6GB", "SettingsTabSystemIgnoreMissingServices": "Αγνόηση υπηρεσιών που λείπουν", "SettingsTabGraphics": "Γραφικά", - "SettingsTabGraphicsEnhancements": "Βελτιώσεις", + "SettingsTabGraphicsAPI": "API Γραφικά", "SettingsTabGraphicsEnableShaderCache": "Ενεργοποίηση Προσωρινής Μνήμης Shader", "SettingsTabGraphicsAnisotropicFiltering": "Ανισότροπο Φιλτράρισμα:", "SettingsTabGraphicsAnisotropicFilteringAuto": "Αυτόματο", @@ -416,7 +416,7 @@ "CommonFavorite": "Αγαπημένα", "OrderAscending": "Αύξουσα", "OrderDescending": "Φθίνουσα", - "SettingsTabGraphicsFeatures": "Χαρακτηριστικά", + "SettingsTabGraphicsFeatures": "Χαρακτηριστικά & Βελτιώσεις", "ErrorWindowTitle": "Παράθυρο σφάλματος", "ToggleDiscordTooltip": "Ενεργοποιεί ή απενεργοποιεί την Εμπλουτισμένη Παρουσία σας στο Discord", "AddGameDirBoxTooltip": "Εισαγάγετε μία τοποθεσία παιχνιδιών για προσθήκη στη λίστα", diff --git a/Ryujinx.Ava/Assets/Locales/en_US.json b/Ryujinx.Ava/Assets/Locales/en_US.json index 1188c6c6..f59e24ce 100644 --- a/Ryujinx.Ava/Assets/Locales/en_US.json +++ b/Ryujinx.Ava/Assets/Locales/en_US.json @@ -122,8 +122,8 @@ "SettingsTabSystemExpandDramSize": "Expand DRAM Size to 6GB", "SettingsTabSystemIgnoreMissingServices": "Ignore Missing Services", "SettingsTabGraphics": "Graphics", - "SettingsTabGraphicsEnhancements": "Enhancements", - "SettingsTabGraphicsEnableShaderCache": "Shader Cache", + "SettingsTabGraphicsAPI": "Graphics API", + "SettingsTabGraphicsEnableShaderCache": "Enable Shader Cache", "SettingsTabGraphicsAnisotropicFiltering": "Anisotropic Filtering:", "SettingsTabGraphicsAnisotropicFilteringAuto": "Auto", "SettingsTabGraphicsAnisotropicFiltering2x": "2x", @@ -416,7 +416,7 @@ "CommonFavorite": "Favorite", "OrderAscending": "Ascending", "OrderDescending": "Descending", - "SettingsTabGraphicsFeatures": "Features", + "SettingsTabGraphicsFeatures": "Features & Enhancements", "ErrorWindowTitle": "Error Window", "ToggleDiscordTooltip": "Choose whether or not to display Ryujinx on your \"currently playing\" Discord activity", "AddGameDirBoxTooltip": "Enter a game directory to add to the list", @@ -579,5 +579,14 @@ "SettingsTabHotkeysResScaleUpHotkey": "Increase resolution:", "SettingsTabHotkeysResScaleDownHotkey": "Decrease resolution:", "UserProfilesName": "Name:", - "UserProfilesUserId" : "User Id:" + "UserProfilesUserId" : "User Id:", + "SettingsTabGraphicsBackend": "Graphics Backend", + "SettingsTabGraphicsBackendTooltip": "Graphics Backend to use", + "SettingsEnableTextureRecompression": "Enable Texture Recompression", + "SettingsEnableTextureRecompressionTooltip": "Compresses certain textures in order to reduce VRAM usage.\n\nRecommended for use with GPUs that have less than 4GB VRAM.\n\nLeave OFF if unsure.", + "SettingsTabGraphicsPreferredGpu": "Preferred GPU", + "SettingsTabGraphicsPreferredGpuTooltip": "Select the graphics card that will be used with the Vulkan graphics backend.\n\nDoes not affect the GPU that OpenGL will use.\n\nSet to the GPU flagged as \"dGPU\" if unsure. If there isn't one, leave untouched.", + "SettingsAppRequiredRestartMessage": "Ryujinx Restart Required", + "SettingsGpuBackendRestartMessage": "Graphics Backend or Gpu settings have been modified. This will require a restart to be applied", + "SettingsGpuBackendRestartSubMessage": "Do you want to restart now?" } diff --git a/Ryujinx.Ava/Assets/Locales/es_ES.json b/Ryujinx.Ava/Assets/Locales/es_ES.json index 363598f0..693bac95 100644 --- a/Ryujinx.Ava/Assets/Locales/es_ES.json +++ b/Ryujinx.Ava/Assets/Locales/es_ES.json @@ -122,8 +122,8 @@ "SettingsTabSystemExpandDramSize": "Expandir DRAM a 6GB", "SettingsTabSystemIgnoreMissingServices": "Ignorar servicios no implementados", "SettingsTabGraphics": "Gráficos", - "SettingsTabGraphicsEnhancements": "Mejoras", - "SettingsTabGraphicsEnableShaderCache": "Caché de sombreadores", + "SettingsTabGraphicsAPI": "API de gráficos", + "SettingsTabGraphicsEnableShaderCache": "Habilitar caché de sombreadores", "SettingsTabGraphicsAnisotropicFiltering": "Filtro anisotrópico:", "SettingsTabGraphicsAnisotropicFilteringAuto": "Auto", "SettingsTabGraphicsAnisotropicFiltering2x": "x2", @@ -416,7 +416,7 @@ "CommonFavorite": "Favorito", "OrderAscending": "Ascendente", "OrderDescending": "Descendente", - "SettingsTabGraphicsFeatures": "Funcionalidades", + "SettingsTabGraphicsFeatures": "Funcionalidades & Mejoras", "ErrorWindowTitle": "Ventana de error", "ToggleDiscordTooltip": "Elige si muestras Ryujinx o no en tu actividad de Discord cuando lo estés usando", "AddGameDirBoxTooltip": "Elige un directorio de juegos para mostrar en la ventana principal", diff --git a/Ryujinx.Ava/Assets/Locales/fr_FR.json b/Ryujinx.Ava/Assets/Locales/fr_FR.json index caafba17..2cd5cfd2 100644 --- a/Ryujinx.Ava/Assets/Locales/fr_FR.json +++ b/Ryujinx.Ava/Assets/Locales/fr_FR.json @@ -115,7 +115,7 @@ "SettingsTabSystemExpandDramSize": "Augmenter la taille de la DRAM à 6GB", "SettingsTabSystemIgnoreMissingServices": "Ignorer les services manquant", "SettingsTabGraphics": "Graphique", - "SettingsTabGraphicsEnhancements": "Améliorations", + "SettingsTabGraphicsAPI": "API Graphique", "SettingsTabGraphicsEnableShaderCache": "Activer le cache des shaders", "SettingsTabGraphicsAnisotropicFiltering": "Filtrage anisotrope:", "SettingsTabGraphicsAnisotropicFilteringAuto": "Auto", @@ -138,6 +138,7 @@ "SettingsTabGraphicsAspectRatioStretch": "Écran étiré", "SettingsTabGraphicsDeveloperOptions": "Options développeur", "SettingsTabGraphicsShaderDumpPath": "Chemin du dossier de dump des shaders:", + "SettingsTabGraphicsFeatures": "Fonctionnalités & Améliorations", "SettingsTabLogging": "Journaux", "SettingsTabLoggingLogging": "Journaux", "SettingsTabLoggingEnableLoggingToFile": "Activer la sauvegarde des journaux vers un fichier", diff --git a/Ryujinx.Ava/Assets/Locales/it_IT.json b/Ryujinx.Ava/Assets/Locales/it_IT.json index 5aca9175..3e403094 100644 --- a/Ryujinx.Ava/Assets/Locales/it_IT.json +++ b/Ryujinx.Ava/Assets/Locales/it_IT.json @@ -122,7 +122,7 @@ "SettingsTabSystemExpandDramSize": "Espandi dimensione DRAM a 6GB", "SettingsTabSystemIgnoreMissingServices": "Ignora servizi mancanti", "SettingsTabGraphics": "Grafica", - "SettingsTabGraphicsEnhancements": "Miglioramenti", + "SettingsTabGraphicsAPI": "API Grafiche", "SettingsTabGraphicsEnableShaderCache": "Attiva Shader Cache", "SettingsTabGraphicsAnisotropicFiltering": "Filtro anisotropico:", "SettingsTabGraphicsAnisotropicFilteringAuto": "Auto", @@ -416,7 +416,7 @@ "CommonFavorite": "Preferito", "OrderAscending": "Crescente", "OrderDescending": "Decrescente", - "SettingsTabGraphicsFeatures": "Funzionalità", + "SettingsTabGraphicsFeatures": "Funzionalità & Miglioramenti", "ErrorWindowTitle": "Finestra errore", "ToggleDiscordTooltip": "Attiva o disattiva Discord Rich Presence", "AddGameDirBoxTooltip": "Inserisci la directory di un gioco per aggiungerlo alla lista", diff --git a/Ryujinx.Ava/Assets/Locales/ko_KR.json b/Ryujinx.Ava/Assets/Locales/ko_KR.json index 7bff6d2b..34253314 100644 --- a/Ryujinx.Ava/Assets/Locales/ko_KR.json +++ b/Ryujinx.Ava/Assets/Locales/ko_KR.json @@ -122,7 +122,7 @@ "SettingsTabSystemExpandDramSize": "DRAM 크기를 6GB로 확장", "SettingsTabSystemIgnoreMissingServices": "누락된 서비스 무시", "SettingsTabGraphics": "제도법", - "SettingsTabGraphicsEnhancements": "개선 사항", + "SettingsTabGraphicsAPI": "그래픽 API", "SettingsTabGraphicsEnableShaderCache": "셰이더 캐시 활성화", "SettingsTabGraphicsAnisotropicFiltering": "이방성 필터링 :", "SettingsTabGraphicsAnisotropicFilteringAuto": "자동적 인", @@ -415,7 +415,7 @@ "CommonFavorite": "가장 좋아하는", "OrderAscending": "오름차순", "OrderDescending": "내림차순", - "SettingsTabGraphicsFeatures": "특징", + "SettingsTabGraphicsFeatures": "특징ㆍ개선 사항", "ErrorWindowTitle": "오류 창", "ToggleDiscordTooltip": "Discord Rich Presence 활성화 또는 비활성화", "AddGameDirBoxTooltip": "게임 디렉토리를 입력하여 목록에 추가하세요", diff --git a/Ryujinx.Ava/Assets/Locales/pt_BR.json b/Ryujinx.Ava/Assets/Locales/pt_BR.json index 368388bb..c5dbe49a 100644 --- a/Ryujinx.Ava/Assets/Locales/pt_BR.json +++ b/Ryujinx.Ava/Assets/Locales/pt_BR.json @@ -122,7 +122,7 @@ "SettingsTabSystemExpandDramSize": "Expandir memória para 6GB", "SettingsTabSystemIgnoreMissingServices": "Ignorar serviços não implementados", "SettingsTabGraphics": "Gráficos", - "SettingsTabGraphicsEnhancements": "Melhorias", + "SettingsTabGraphicsAPI": "API gráfica", "SettingsTabGraphicsEnableShaderCache": "Habilitar cache de shader", "SettingsTabGraphicsAnisotropicFiltering": "Filtragem anisotrópica:", "SettingsTabGraphicsAnisotropicFilteringAuto": "Auto", @@ -416,7 +416,7 @@ "CommonFavorite": "Favorito", "OrderAscending": "Ascendente", "OrderDescending": "Descendente", - "SettingsTabGraphicsFeatures": "Recursos", + "SettingsTabGraphicsFeatures": "Recursos & Melhorias", "ErrorWindowTitle": "Janela de erro", "ToggleDiscordTooltip": "Habilita ou desabilita Discord Rich Presence", "AddGameDirBoxTooltip": "Escreva um diretório de jogo para adicionar à lista", diff --git a/Ryujinx.Ava/Assets/Locales/ru_RU.json b/Ryujinx.Ava/Assets/Locales/ru_RU.json index d63ed8dd..62835f0a 100644 --- a/Ryujinx.Ava/Assets/Locales/ru_RU.json +++ b/Ryujinx.Ava/Assets/Locales/ru_RU.json @@ -122,7 +122,7 @@ "SettingsTabSystemExpandDramSize": "Увеличение размера DRAM до 6GB", "SettingsTabSystemIgnoreMissingServices": "Игнорировать отсутствующие службы", "SettingsTabGraphics": "Графика", - "SettingsTabGraphicsEnhancements": "Улучшения", + "SettingsTabGraphicsAPI": "Графические API", "SettingsTabGraphicsEnableShaderCache": "Включить кэш шейдеров", "SettingsTabGraphicsAnisotropicFiltering": "Анизотропная фильтрация:", "SettingsTabGraphicsAnisotropicFilteringAuto": "Автоматически", @@ -377,7 +377,7 @@ "DialogUpdateAddUpdateErrorMessage": "Указанный файл не содержит обновления для выбранного заголовка!", "DialogSettingsBackendThreadingWarningTitle": "Предупреждение: многопоточность в бэкенде", "DialogSettingsBackendThreadingWarningMessage": "Ryujinx необходимо перезапустить после изменения этой опции, чтобы она полностью применилась. В зависимости от вашей платформы вам может потребоваться вручную отключить собственную многопоточность вашего драйвера при использовании Ryujinx.", - "SettingsTabGraphicsFeaturesOptions": "Функции", + "SettingsTabGraphicsFeaturesOptions": "Функции & Улучшения", "SettingsTabGraphicsBackendMultithreading": "Многопоточность графического бэкенда:", "CommonAuto": "Автоматически", "CommonOff": "Выключен", diff --git a/Ryujinx.Ava/Assets/Locales/tr_TR.json b/Ryujinx.Ava/Assets/Locales/tr_TR.json index c8b5e1cd..19d43020 100644 --- a/Ryujinx.Ava/Assets/Locales/tr_TR.json +++ b/Ryujinx.Ava/Assets/Locales/tr_TR.json @@ -122,7 +122,7 @@ "SettingsTabSystemExpandDramSize": "DRAM boyutunu 6GB'a genişlet", "SettingsTabSystemIgnoreMissingServices": "Eksik Servisleri Görmezden Gel", "SettingsTabGraphics": "Grafikler", - "SettingsTabGraphicsEnhancements": "İyileştirmeler", + "SettingsTabGraphicsAPI": "Grafikler API", "SettingsTabGraphicsEnableShaderCache": "Shader Cache'i Etkinleştir", "SettingsTabGraphicsAnisotropicFiltering": "Anisotropic Filtering:", "SettingsTabGraphicsAnisotropicFilteringAuto": "Otomatik", @@ -416,7 +416,7 @@ "CommonFavorite": "Favori", "OrderAscending": "Artan", "OrderDescending": "Azalan", - "SettingsTabGraphicsFeatures": "Özellikler", + "SettingsTabGraphicsFeatures": "Özellikler & İyileştirmeler", "ErrorWindowTitle": "Hata Penceresi", "ToggleDiscordTooltip": "Discord Rich Presence'i Aç/Kapat", "AddGameDirBoxTooltip": "Listeye eklemek için bir oyun dizini ekleyin", diff --git a/Ryujinx.Ava/Assets/Locales/zh_CN.json b/Ryujinx.Ava/Assets/Locales/zh_CN.json index 78bc3fde..f91c093b 100644 --- a/Ryujinx.Ava/Assets/Locales/zh_CN.json +++ b/Ryujinx.Ava/Assets/Locales/zh_CN.json @@ -122,7 +122,7 @@ "SettingsTabSystemExpandDramSize": "将模拟RAM大小扩展到 6GB",
"SettingsTabSystemIgnoreMissingServices": "忽略缺少的服务",
"SettingsTabGraphics": "图像",
- "SettingsTabGraphicsEnhancements": "增强",
+ "SettingsTabGraphicsAPI": "的图形 API",
"SettingsTabGraphicsEnableShaderCache": "启用着色器缓存",
"SettingsTabGraphicsAnisotropicFiltering": "各向异性过滤:",
"SettingsTabGraphicsAnisotropicFilteringAuto": "自动",
@@ -416,7 +416,7 @@ "CommonFavorite": "收藏",
"OrderAscending": "从小到大",
"OrderDescending": "从大到小",
- "SettingsTabGraphicsFeatures": "额外功能",
+ "SettingsTabGraphicsFeatures": "额外功能和增强",
"ErrorWindowTitle": "错误窗口",
"ToggleDiscordTooltip": "启用或关闭 Discord 详细在线状态展示",
"AddGameDirBoxTooltip": "输入要添加的游戏目录",
diff --git a/Ryujinx.Ava/Program.cs b/Ryujinx.Ava/Program.cs index 8af7af7a..be27e9cd 100644 --- a/Ryujinx.Ava/Program.cs +++ b/Ryujinx.Ava/Program.cs @@ -3,6 +3,7 @@ using Avalonia; using Avalonia.OpenGL; using Avalonia.Rendering; using Avalonia.Threading; +using Ryujinx.Ava.Ui.Backend; using Ryujinx.Ava.Ui.Controls; using Ryujinx.Ava.Ui.Windows; using Ryujinx.Common; @@ -11,9 +12,12 @@ using Ryujinx.Common.GraphicsDriver; using Ryujinx.Common.Logging; using Ryujinx.Common.System; using Ryujinx.Common.SystemInfo; +using Ryujinx.Graphics.Vulkan; using Ryujinx.Modules; using Ryujinx.Ui.Common; using Ryujinx.Ui.Common.Configuration; +using Silk.NET.Vulkan.Extensions.EXT; +using Silk.NET.Vulkan.Extensions.KHR; using System; using System.Collections.Generic; using System.IO; @@ -25,17 +29,20 @@ namespace Ryujinx.Ava internal class Program { public static double WindowScaleFactor { get; set; } + public static double ActualScaleFactor { get; set; } public static string Version { get; private set; } public static string ConfigurationPath { get; private set; } public static string CommandLineProfile { get; set; } public static bool PreviewerDetached { get; private set; } public static RenderTimer RenderTimer { get; private set; } + public static bool UseVulkan { get; private set; } [DllImport("user32.dll", SetLastError = true)] public static extern int MessageBoxA(IntPtr hWnd, string text, string caption, uint type); private const uint MB_ICONWARNING = 0x30; + private const int BaseDpi = 96; public static void Main(string[] args) { @@ -66,7 +73,7 @@ namespace Ryujinx.Ava EnableMultiTouch = true, EnableIme = true, UseEGL = false, - UseGpu = true, + UseGpu = !UseVulkan, GlProfiles = new List<GlVersion>() { new GlVersion(GlProfileType.OpenGL, 4, 3) @@ -75,7 +82,7 @@ namespace Ryujinx.Ava .With(new Win32PlatformOptions { EnableMultitouch = true, - UseWgl = true, + UseWgl = !UseVulkan, WglProfiles = new List<GlVersion>() { new GlVersion(GlProfileType.OpenGL, 4, 3) @@ -84,6 +91,19 @@ namespace Ryujinx.Ava CompositionBackdropCornerRadius = 8f, }) .UseSkia() + .With(new Ui.Vulkan.VulkanOptions() + { + ApplicationName = "Ryujinx.Graphics.Vulkan", + VulkanVersion = new Version(1, 2), + MaxQueueCount = 2, + PreferDiscreteGpu = true, + PreferredDevice = !PreviewerDetached ? "" : ConfigurationState.Instance.Graphics.PreferredGpu.Value, + UseDebug = !PreviewerDetached ? false : ConfigurationState.Instance.Logger.GraphicsDebugLevel.Value != GraphicsDebugLevel.None, + }) + .With(new SkiaOptions() + { + CustomGpuFactory = UseVulkan ? SkiaGpuFactory.CreateVulkanGpu : null + }) .AfterSetup(_ => { AvaloniaLocator.CurrentMutable @@ -136,9 +156,6 @@ namespace Ryujinx.Ava } } - // Make process DPI aware for proper window sizing on high-res screens. - WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor(); - // Delete backup files after updating. Task.Run(Updater.CleanupUpdate); @@ -162,6 +179,18 @@ namespace Ryujinx.Ava ReloadConfig(); + UseVulkan = PreviewerDetached ? ConfigurationState.Instance.Graphics.GraphicsBackend.Value == GraphicsBackend.Vulkan : false; + + if (UseVulkan) + { + // With a custom gpu backend, avalonia doesn't enable dpi awareness, so the backend must handle it. This isn't so for the opengl backed, + // as that uses avalonia's gpu backend and it's enabled there. + ForceDpiAware.Windows(); + } + + WindowScaleFactor = ForceDpiAware.GetWindowScaleFactor(); + ActualScaleFactor = ForceDpiAware.GetActualScaleFactor() / BaseDpi; + // Logging system information. PrintSystemInfo(); diff --git a/Ryujinx.Ava/Ryujinx.Ava.csproj b/Ryujinx.Ava/Ryujinx.Ava.csproj index 193e839f..dfb50292 100644 --- a/Ryujinx.Ava/Ryujinx.Ava.csproj +++ b/Ryujinx.Ava/Ryujinx.Ava.csproj @@ -28,9 +28,12 @@ <PackageReference Include="FluentAvaloniaUI" Version="1.4.1" /> <PackageReference Include="XamlNameReferenceGenerator" Version="1.3.4" /> + <PackageReference Include="OpenTK.Core" Version="4.7.2" /> <PackageReference Include="Ryujinx.Audio.OpenAL.Dependencies" Version="1.21.0.1" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'" /> <PackageReference Include="Ryujinx.Graphics.Nvdec.Dependencies" Version="5.0.1-build10" Condition="'$(RuntimeIdentifier)' != 'linux-x64' AND '$(RuntimeIdentifier)' != 'osx-x64'" /> - <PackageReference Include="OpenTK.Graphics" Version="4.7.2" /> + <PackageReference Include="Silk.NET.Vulkan" Version="2.10.1" /> + <PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.10.1" /> + <PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.10.1" /> <PackageReference Include="SPB" Version="0.0.4-build17" /> <PackageReference Include="SharpZipLib" Version="1.3.3" /> <PackageReference Include="SixLabors.ImageSharp" Version="1.0.4" /> @@ -38,6 +41,7 @@ <ItemGroup> <ProjectReference Include="..\Ryujinx.Audio.Backends.SDL2\Ryujinx.Audio.Backends.SDL2.csproj" /> + <ProjectReference Include="..\Ryujinx.Graphics.Vulkan\Ryujinx.Graphics.Vulkan.csproj" /> <ProjectReference Include="..\Ryujinx.Input\Ryujinx.Input.csproj" /> <ProjectReference Include="..\Ryujinx.Input.SDL2\Ryujinx.Input.SDL2.csproj" /> <ProjectReference Include="..\Ryujinx.Audio.Backends.OpenAL\Ryujinx.Audio.Backends.OpenAL.csproj" /> diff --git a/Ryujinx.Ava/Ui/Applet/AvaloniaDynamicTextInputHandler.cs b/Ryujinx.Ava/Ui/Applet/AvaloniaDynamicTextInputHandler.cs index 294e8965..02a99c1d 100644 --- a/Ryujinx.Ava/Ui/Applet/AvaloniaDynamicTextInputHandler.cs +++ b/Ryujinx.Ava/Ui/Applet/AvaloniaDynamicTextInputHandler.cs @@ -135,7 +135,7 @@ namespace Ryujinx.Ava.Ui.Applet Dispatcher.UIThread.Post(() => { _hiddenTextBox.Clear(); - _parent.GlRenderer.Focus(); + _parent.RendererControl.Focus(); _parent = null; }); diff --git a/Ryujinx.Ava/Ui/Backend/BackendSurface.cs b/Ryujinx.Ava/Ui/Backend/BackendSurface.cs new file mode 100644 index 00000000..423fe038 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/BackendSurface.cs @@ -0,0 +1,76 @@ +using Avalonia; +using System; +using System.Runtime.InteropServices; +using static Ryujinx.Ava.Ui.Backend.Interop; + +namespace Ryujinx.Ava.Ui.Backend +{ + public abstract class BackendSurface : IDisposable + { + protected IntPtr Display => _display; + + private IntPtr _display = IntPtr.Zero; + + [DllImport("libX11.so.6")] + public static extern IntPtr XOpenDisplay(IntPtr display); + + [DllImport("libX11.so.6")] + public static extern int XCloseDisplay(IntPtr display); + + private PixelSize _currentSize; + public IntPtr Handle { get; protected set; } + + public bool IsDisposed { get; private set; } + + public BackendSurface(IntPtr handle) + { + Handle = handle; + + if (OperatingSystem.IsLinux()) + { + _display = XOpenDisplay(IntPtr.Zero); + } + } + + public PixelSize Size + { + get + { + PixelSize size = new PixelSize(); + if (OperatingSystem.IsWindows()) + { + GetClientRect(Handle, out var rect); + size = new PixelSize(rect.right, rect.bottom); + } + else if (OperatingSystem.IsLinux()) + { + XWindowAttributes attributes = new XWindowAttributes(); + XGetWindowAttributes(Display, Handle, ref attributes); + + size = new PixelSize(attributes.width, attributes.height); + } + + _currentSize = size; + + return size; + } + } + + public PixelSize CurrentSize => _currentSize; + + public virtual void Dispose() + { + if (IsDisposed) + { + throw new ObjectDisposedException(nameof(BackendSurface)); + } + + IsDisposed = true; + + if (_display != IntPtr.Zero) + { + XCloseDisplay(_display); + } + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Backend/Interop.cs b/Ryujinx.Ava/Ui/Backend/Interop.cs new file mode 100644 index 00000000..617e9767 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Interop.cs @@ -0,0 +1,49 @@ +using FluentAvalonia.Interop; +using System; +using System.Runtime.InteropServices; + +namespace Ryujinx.Ava.Ui.Backend +{ + public static class Interop + { + [StructLayout(LayoutKind.Sequential)] + public struct XWindowAttributes + { + public int x; + public int y; + public int width; + public int height; + public int border_width; + public int depth; + public IntPtr visual; + public IntPtr root; + public int c_class; + public int bit_gravity; + public int win_gravity; + public int backing_store; + public IntPtr backing_planes; + public IntPtr backing_pixel; + public int save_under; + public IntPtr colormap; + public int map_installed; + public int map_state; + public IntPtr all_event_masks; + public IntPtr your_event_mask; + public IntPtr do_not_propagate_mask; + public int override_direct; + public IntPtr screen; + } + + [DllImport("user32.dll")] + public static extern bool GetClientRect(IntPtr hwnd, out RECT lpRect); + + [DllImport("libX11.so.6")] + public static extern int XCloseDisplay(IntPtr display); + + [DllImport("libX11.so.6")] + public static extern int XGetWindowAttributes(IntPtr display, IntPtr window, ref XWindowAttributes attributes); + + [DllImport("libX11.so.6")] + public static extern IntPtr XOpenDisplay(IntPtr display); + } +} diff --git a/Ryujinx.Ava/Ui/Backend/SkiaGpuFactory.cs b/Ryujinx.Ava/Ui/Backend/SkiaGpuFactory.cs new file mode 100644 index 00000000..335bc905 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/SkiaGpuFactory.cs @@ -0,0 +1,26 @@ +using Avalonia; +using Avalonia.Skia; +using Ryujinx.Ava.Ui.Vulkan; +using Ryujinx.Ava.Ui.Backend.Vulkan; + +namespace Ryujinx.Ava.Ui.Backend +{ + public static class SkiaGpuFactory + { + public static ISkiaGpu CreateVulkanGpu() + { + var skiaOptions = AvaloniaLocator.Current.GetService<SkiaOptions>() ?? new SkiaOptions(); + var platformInterface = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>(); + + if (platformInterface == null) + { + VulkanPlatformInterface.TryInitialize(); + } + + var gpu = new VulkanSkiaGpu(skiaOptions.MaxGpuResourceSizeBytes); + AvaloniaLocator.CurrentMutable.Bind<VulkanSkiaGpu>().ToConstant(gpu); + + return gpu; + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/ResultExtensions.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/ResultExtensions.cs new file mode 100644 index 00000000..b1326dbf --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/ResultExtensions.cs @@ -0,0 +1,16 @@ +using System; +using Silk.NET.Vulkan; + +namespace Ryujinx.Ava.Ui.Vulkan +{ + public static class ResultExtensions + { + public static void ThrowOnError(this Result result) + { + if (result != Result.Success) + { + throw new Exception($"Unexpected API error \"{result}\"."); + } + } + } +} diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanRenderTarget.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanRenderTarget.cs new file mode 100644 index 00000000..ba7ddc7a --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanRenderTarget.cs @@ -0,0 +1,135 @@ +using System; +using Avalonia.Skia; +using Ryujinx.Ava.Ui.Vulkan; +using Ryujinx.Ava.Ui.Vulkan.Surfaces; +using SkiaSharp; + +namespace Ryujinx.Ava.Ui.Backend.Vulkan +{ + internal class VulkanRenderTarget : ISkiaGpuRenderTarget + { + public GRContext GrContext { get; set; } + + private readonly VulkanSurfaceRenderTarget _surface; + private readonly IVulkanPlatformSurface _vulkanPlatformSurface; + + public VulkanRenderTarget(VulkanPlatformInterface vulkanPlatformInterface, IVulkanPlatformSurface vulkanPlatformSurface) + { + _surface = vulkanPlatformInterface.CreateRenderTarget(vulkanPlatformSurface); + _vulkanPlatformSurface = vulkanPlatformSurface; + } + + public void Dispose() + { + _surface.Dispose(); + } + + public ISkiaGpuRenderSession BeginRenderingSession() + { + var session = _surface.BeginDraw(_vulkanPlatformSurface.Scaling); + bool success = false; + try + { + var disp = session.Display; + var api = session.Api; + + var size = session.Size; + var scaling = session.Scaling; + if (size.Width <= 0 || size.Height <= 0 || scaling < 0) + { + size = new Avalonia.PixelSize(1, 1); + scaling = 1; + } + + lock (GrContext) + { + GrContext.ResetContext(); + + var imageInfo = new GRVkImageInfo() + { + CurrentQueueFamily = disp.QueueFamilyIndex, + Format = _surface.ImageFormat, + Image = _surface.Image.Handle, + ImageLayout = (uint)_surface.Image.CurrentLayout, + ImageTiling = (uint)_surface.Image.Tiling, + ImageUsageFlags = _surface.UsageFlags, + LevelCount = _surface.MipLevels, + SampleCount = 1, + Protected = false, + Alloc = new GRVkAlloc() + { + Memory = _surface.Image.MemoryHandle, + Flags = 0, + Offset = 0, + Size = _surface.MemorySize + } + }; + + var renderTarget = + new GRBackendRenderTarget((int)size.Width, (int)size.Height, 1, + imageInfo); + var surface = SKSurface.Create(GrContext, renderTarget, + GRSurfaceOrigin.TopLeft, + _surface.IsRgba ? SKColorType.Rgba8888 : SKColorType.Bgra8888, SKColorSpace.CreateSrgb()); + + if (surface == null) + { + throw new InvalidOperationException( + "Surface can't be created with the provided render target"); + } + + success = true; + + return new VulkanGpuSession(GrContext, renderTarget, surface, session); + } + } + finally + { + if (!success) + { + session.Dispose(); + } + } + } + + public bool IsCorrupted { get; } + + internal class VulkanGpuSession : ISkiaGpuRenderSession + { + private readonly GRBackendRenderTarget _backendRenderTarget; + private readonly VulkanSurfaceRenderingSession _vulkanSession; + + public VulkanGpuSession(GRContext grContext, + GRBackendRenderTarget backendRenderTarget, + SKSurface surface, + VulkanSurfaceRenderingSession vulkanSession) + { + GrContext = grContext; + _backendRenderTarget = backendRenderTarget; + SkSurface = surface; + _vulkanSession = vulkanSession; + + SurfaceOrigin = GRSurfaceOrigin.TopLeft; + } + + public void Dispose() + { + lock (_vulkanSession.Display.Lock) + { + SkSurface.Canvas.Flush(); + + SkSurface.Dispose(); + _backendRenderTarget.Dispose(); + GrContext.Flush(); + + _vulkanSession.Dispose(); + } + } + + public GRContext GrContext { get; } + public SKSurface SkSurface { get; } + public double ScaleFactor => _vulkanSession.Scaling; + public GRSurfaceOrigin SurfaceOrigin { get; } + } + } +} diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSkiaGpu.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSkiaGpu.cs new file mode 100644 index 00000000..4fc6b929 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSkiaGpu.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using Avalonia; +using Avalonia.Platform; +using Avalonia.Skia; +using Avalonia.X11; +using Ryujinx.Ava.Ui.Vulkan; +using Silk.NET.Vulkan; +using SkiaSharp; + +namespace Ryujinx.Ava.Ui.Backend.Vulkan +{ + public class VulkanSkiaGpu : ISkiaGpu + { + private readonly VulkanPlatformInterface _vulkan; + private readonly long? _maxResourceBytes; + private GRVkBackendContext _grVkBackend; + private bool _initialized; + + public GRContext GrContext { get; private set; } + + public VulkanSkiaGpu(long? maxResourceBytes) + { + _vulkan = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>(); + _maxResourceBytes = maxResourceBytes; + } + + private void Initialize() + { + if (_initialized) + { + return; + } + + _initialized = true; + GRVkGetProcedureAddressDelegate getProc = (string name, IntPtr instanceHandle, IntPtr deviceHandle) => + { + IntPtr addr = IntPtr.Zero; + + if (deviceHandle != IntPtr.Zero) + { + addr = _vulkan.Device.Api.GetDeviceProcAddr(new Device(deviceHandle), name); + + if (addr != IntPtr.Zero) + { + return addr; + } + + addr = _vulkan.Device.Api.GetDeviceProcAddr(new Device(_vulkan.Device.Handle), name); + + if (addr != IntPtr.Zero) + { + return addr; + } + } + + addr = _vulkan.Device.Api.GetInstanceProcAddr(new Instance(_vulkan.Instance.Handle), name); + + if (addr == IntPtr.Zero) + { + addr = _vulkan.Device.Api.GetInstanceProcAddr(new Instance(instanceHandle), name); + } + + return addr; + }; + + _grVkBackend = new GRVkBackendContext() + { + VkInstance = _vulkan.Device.Handle, + VkPhysicalDevice = _vulkan.PhysicalDevice.Handle, + VkDevice = _vulkan.Device.Handle, + VkQueue = _vulkan.Device.Queue.Handle, + GraphicsQueueIndex = _vulkan.PhysicalDevice.QueueFamilyIndex, + GetProcedureAddress = getProc + }; + GrContext = GRContext.CreateVulkan(_grVkBackend); + if (_maxResourceBytes.HasValue) + { + GrContext.SetResourceCacheLimit(_maxResourceBytes.Value); + } + } + + public ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable<object> surfaces) + { + foreach (var surface in surfaces) + { + VulkanWindowSurface window; + + if (surface is IPlatformHandle handle) + { + window = new VulkanWindowSurface(handle.Handle); + } + else if (surface is X11FramebufferSurface x11FramebufferSurface) + { + // As of Avalonia 0.10.13, an IPlatformHandle isn't passed for linux, so use reflection to otherwise get the window id + var xId = (IntPtr)x11FramebufferSurface.GetType().GetField( + "_xid", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(x11FramebufferSurface); + + window = new VulkanWindowSurface(xId); + } + else + { + continue; + } + + VulkanRenderTarget vulkanRenderTarget = new VulkanRenderTarget(_vulkan, window); + + Initialize(); + + vulkanRenderTarget.GrContext = GrContext; + + return vulkanRenderTarget; + } + + return null; + } + + public ISkiaSurface TryCreateSurface(PixelSize size, ISkiaGpuRenderSession session) + { + return null; + } + } +} diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSurface.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSurface.cs new file mode 100644 index 00000000..fd2d379b --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/Skia/VulkanSurface.cs @@ -0,0 +1,53 @@ +using Avalonia; +using Ryujinx.Ava.Ui.Vulkan; +using Ryujinx.Ava.Ui.Vulkan.Surfaces; +using Silk.NET.Vulkan; +using Silk.NET.Vulkan.Extensions.KHR; +using System; + +namespace Ryujinx.Ava.Ui.Backend.Vulkan +{ + internal class VulkanWindowSurface : BackendSurface, IVulkanPlatformSurface + { + public float Scaling => (float)Program.ActualScaleFactor; + + public PixelSize SurfaceSize => Size; + + public VulkanWindowSurface(IntPtr handle) : base(handle) + { + } + + public unsafe SurfaceKHR CreateSurface(VulkanInstance instance) + { + if (OperatingSystem.IsWindows()) + { + if (instance.Api.TryGetInstanceExtension(new Instance(instance.Handle), out KhrWin32Surface surfaceExtension)) + { + var createInfo = new Win32SurfaceCreateInfoKHR() { Hinstance = 0, Hwnd = Handle, SType = StructureType.Win32SurfaceCreateInfoKhr }; + + surfaceExtension.CreateWin32Surface(new Instance(instance.Handle), createInfo, null, out var surface).ThrowOnError(); + + return surface; + } + } + else if (OperatingSystem.IsLinux()) + { + if (instance.Api.TryGetInstanceExtension(new Instance(instance.Handle), out KhrXlibSurface surfaceExtension)) + { + var createInfo = new XlibSurfaceCreateInfoKHR() + { + SType = StructureType.XlibSurfaceCreateInfoKhr, + Dpy = (nint*)Display, + Window = Handle + }; + + surfaceExtension.CreateXlibSurface(new Instance(instance.Handle), createInfo, null, out var surface).ThrowOnError(); + + return surface; + } + } + + throw new PlatformNotSupportedException("The current platform does not support surface creation."); + } + } +}
\ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/IVulkanPlatformSurface.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/IVulkanPlatformSurface.cs new file mode 100644 index 00000000..642d8a6a --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/IVulkanPlatformSurface.cs @@ -0,0 +1,13 @@ +using System; +using Avalonia; +using Silk.NET.Vulkan; + +namespace Ryujinx.Ava.Ui.Vulkan.Surfaces +{ + public interface IVulkanPlatformSurface : IDisposable + { + float Scaling { get; } + PixelSize SurfaceSize { get; } + SurfaceKHR CreateSurface(VulkanInstance instance); + } +} diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/VulkanSurfaceRenderTarget.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/VulkanSurfaceRenderTarget.cs new file mode 100644 index 00000000..b2b8843d --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/Surfaces/VulkanSurfaceRenderTarget.cs @@ -0,0 +1,92 @@ +using System; +using Avalonia; +using Silk.NET.Vulkan; + +namespace Ryujinx.Ava.Ui.Vulkan.Surfaces +{ + internal class VulkanSurfaceRenderTarget : IDisposable + { + private readonly VulkanPlatformInterface _platformInterface; + + private readonly Format _format; + + public VulkanImage Image { get; private set; } + public bool IsCorrupted { get; private set; } = true; + + public uint MipLevels => Image.MipLevels; + + public VulkanSurfaceRenderTarget(VulkanPlatformInterface platformInterface, VulkanSurface surface) + { + _platformInterface = platformInterface; + + Display = VulkanDisplay.CreateDisplay(platformInterface.Instance, platformInterface.Device, + platformInterface.PhysicalDevice, surface); + Surface = surface; + + // Skia seems to only create surfaces from images with unorm format + + IsRgba = Display.SurfaceFormat.Format >= Format.R8G8B8A8Unorm && + Display.SurfaceFormat.Format <= Format.R8G8B8A8Srgb; + + _format = IsRgba ? Format.R8G8B8A8Unorm : Format.B8G8R8A8Unorm; + } + + public bool IsRgba { get; } + + public uint ImageFormat => (uint) _format; + + public ulong MemorySize => Image.MemorySize; + + public VulkanDisplay Display { get; } + + public VulkanSurface Surface { get; } + + public uint UsageFlags => Image.UsageFlags; + + public PixelSize Size { get; private set; } + + public void Dispose() + { + _platformInterface.Device.WaitIdle(); + DestroyImage(); + Display?.Dispose(); + Surface?.Dispose(); + } + + public VulkanSurfaceRenderingSession BeginDraw(float scaling) + { + var session = new VulkanSurfaceRenderingSession(Display, _platformInterface.Device, this, scaling); + + if (IsCorrupted) + { + IsCorrupted = false; + DestroyImage(); + CreateImage(); + } + else + { + Image.TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr); + } + + return session; + } + + public void Invalidate() + { + IsCorrupted = true; + } + + private void CreateImage() + { + Size = Display.Size; + + Image = new VulkanImage(_platformInterface.Device, _platformInterface.PhysicalDevice, _platformInterface.Device.CommandBufferPool, ImageFormat, Size); + } + + private void DestroyImage() + { + _platformInterface.Device.WaitIdle(); + Image?.Dispose(); + } + } +} diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanCommandBufferPool.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanCommandBufferPool.cs new file mode 100644 index 00000000..240035ca --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanCommandBufferPool.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using Silk.NET.Vulkan; + +namespace Ryujinx.Ava.Ui.Vulkan +{ + internal class VulkanCommandBufferPool : IDisposable + { + private readonly VulkanDevice _device; + private readonly CommandPool _commandPool; + + private readonly List<VulkanCommandBuffer> _usedCommandBuffers = new(); + + public unsafe VulkanCommandBufferPool(VulkanDevice device, VulkanPhysicalDevice physicalDevice) + { + _device = device; + + var commandPoolCreateInfo = new CommandPoolCreateInfo + { + SType = StructureType.CommandPoolCreateInfo, + Flags = CommandPoolCreateFlags.CommandPoolCreateResetCommandBufferBit, + QueueFamilyIndex = physicalDevice.QueueFamilyIndex + }; + + device.Api.CreateCommandPool(_device.InternalHandle, commandPoolCreateInfo, null, out _commandPool) + .ThrowOnError(); + } + + private CommandBuffer AllocateCommandBuffer() + { + var commandBufferAllocateInfo = new CommandBufferAllocateInfo + { + SType = StructureType.CommandBufferAllocateInfo, + CommandPool = _commandPool, + CommandBufferCount = 1, + Level = CommandBufferLevel.Primary + }; + + _device.Api.AllocateCommandBuffers(_device.InternalHandle, commandBufferAllocateInfo, out var commandBuffer); + + return commandBuffer; + } + + public VulkanCommandBuffer CreateCommandBuffer() + { + return new(_device, this); + } + + public void FreeUsedCommandBuffers() + { + lock (_usedCommandBuffers) + { + foreach (var usedCommandBuffer in _usedCommandBuffers) + { + usedCommandBuffer.Dispose(); + } + + _usedCommandBuffers.Clear(); + } + } + + private void DisposeCommandBuffer(VulkanCommandBuffer commandBuffer) + { + lock (_usedCommandBuffers) + { + _usedCommandBuffers.Add(commandBuffer); + } + } + + public void Dispose() + { + FreeUsedCommandBuffers(); + _device.Api.DestroyCommandPool(_device.InternalHandle, _commandPool, Span<AllocationCallbacks>.Empty); + } + + public class VulkanCommandBuffer : IDisposable + { + private readonly VulkanCommandBufferPool _commandBufferPool; + private readonly VulkanDevice _device; + private readonly Fence _fence; + private bool _hasEnded; + private bool _hasStarted; + + public IntPtr Handle => InternalHandle.Handle; + + internal CommandBuffer InternalHandle { get; } + + internal unsafe VulkanCommandBuffer(VulkanDevice device, VulkanCommandBufferPool commandBufferPool) + { + _device = device; + _commandBufferPool = commandBufferPool; + + InternalHandle = _commandBufferPool.AllocateCommandBuffer(); + + var fenceCreateInfo = new FenceCreateInfo() + { + SType = StructureType.FenceCreateInfo, + Flags = FenceCreateFlags.FenceCreateSignaledBit + }; + + device.Api.CreateFence(device.InternalHandle, fenceCreateInfo, null, out _fence); + } + + public void BeginRecording() + { + if (!_hasStarted) + { + _hasStarted = true; + + var beginInfo = new CommandBufferBeginInfo + { + SType = StructureType.CommandBufferBeginInfo, + Flags = CommandBufferUsageFlags.CommandBufferUsageOneTimeSubmitBit + }; + + _device.Api.BeginCommandBuffer(InternalHandle, beginInfo); + } + } + + public void EndRecording() + { + if (_hasStarted && !_hasEnded) + { + _hasEnded = true; + + _device.Api.EndCommandBuffer(InternalHandle); + } + } + + public void Submit() + { + Submit(null, null, null, _fence); + } + + public unsafe void Submit( + ReadOnlySpan<Semaphore> waitSemaphores, + ReadOnlySpan<PipelineStageFlags> waitDstStageMask, + ReadOnlySpan<Semaphore> signalSemaphores, + Fence? fence = null) + { + EndRecording(); + + if (!fence.HasValue) + { + fence = _fence; + } + + fixed (Semaphore* pWaitSemaphores = waitSemaphores, pSignalSemaphores = signalSemaphores) + { + fixed (PipelineStageFlags* pWaitDstStageMask = waitDstStageMask) + { + var commandBuffer = InternalHandle; + var submitInfo = new SubmitInfo + { + SType = StructureType.SubmitInfo, + WaitSemaphoreCount = waitSemaphores != null ? (uint)waitSemaphores.Length : 0, + PWaitSemaphores = pWaitSemaphores, + PWaitDstStageMask = pWaitDstStageMask, + CommandBufferCount = 1, + PCommandBuffers = &commandBuffer, + SignalSemaphoreCount = signalSemaphores != null ? (uint)signalSemaphores.Length : 0, + PSignalSemaphores = pSignalSemaphores, + }; + + _device.Api.ResetFences(_device.InternalHandle, 1, fence.Value); + + _device.Submit(submitInfo, fence.Value); + } + } + + _commandBufferPool.DisposeCommandBuffer(this); + } + + public void Dispose() + { + _device.Api.WaitForFences(_device.InternalHandle, 1, _fence, true, ulong.MaxValue); + _device.Api.FreeCommandBuffers(_device.InternalHandle, _commandBufferPool._commandPool, 1, InternalHandle); + _device.Api.DestroyFence(_device.InternalHandle, _fence, Span<AllocationCallbacks>.Empty); + } + } + } +} diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDevice.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDevice.cs new file mode 100644 index 00000000..b03fd720 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDevice.cs @@ -0,0 +1,67 @@ +using System; +using Silk.NET.Vulkan; + +namespace Ryujinx.Ava.Ui.Vulkan +{ + internal class VulkanDevice : IDisposable + { + private static object _lock = new object(); + + public VulkanDevice(Device apiHandle, VulkanPhysicalDevice physicalDevice, Vk api) + { + InternalHandle = apiHandle; + Api = api; + + api.GetDeviceQueue(apiHandle, physicalDevice.QueueFamilyIndex, 0, out var queue); + + var vulkanQueue = new VulkanQueue(this, queue); + Queue = vulkanQueue; + + PresentQueue = vulkanQueue; + + CommandBufferPool = new VulkanCommandBufferPool(this, physicalDevice); + } + + public IntPtr Handle => InternalHandle.Handle; + + internal Device InternalHandle { get; } + public Vk Api { get; } + + public VulkanQueue Queue { get; private set; } + public VulkanQueue PresentQueue { get; } + public VulkanCommandBufferPool CommandBufferPool { get; } + + public void Dispose() + { + WaitIdle(); + CommandBufferPool?.Dispose(); + Queue = null; + } + + internal void Submit(SubmitInfo submitInfo, Fence fence = default) + { + lock (_lock) + { + Api.QueueSubmit(Queue.InternalHandle, 1, submitInfo, fence).ThrowOnError(); + } + } + + public void WaitIdle() + { + lock (_lock) + { + Api.DeviceWaitIdle(InternalHandle); + } + } + + public void QueueWaitIdle() + { + lock (_lock) + { + Api.QueueWaitIdle(Queue.InternalHandle); + } + } + + public object Lock => _lock; + } +} diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDisplay.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDisplay.cs new file mode 100644 index 00000000..bfe5b5a6 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanDisplay.cs @@ -0,0 +1,439 @@ +using System; +using System.Linq; +using System.Threading; +using Avalonia; +using Ryujinx.Ava.Ui.Vulkan.Surfaces; +using Ryujinx.Ui.Common.Configuration; +using Silk.NET.Vulkan; +using Silk.NET.Vulkan.Extensions.KHR; + +namespace Ryujinx.Ava.Ui.Vulkan +{ + internal class VulkanDisplay : IDisposable + { + private static KhrSwapchain _swapchainExtension; + private readonly VulkanInstance _instance; + private readonly VulkanPhysicalDevice _physicalDevice; + private readonly VulkanSemaphorePair _semaphorePair; + private uint _nextImage; + private readonly VulkanSurface _surface; + private SurfaceFormatKHR _surfaceFormat; + private SwapchainKHR _swapchain; + private Extent2D _swapchainExtent; + private Image[] _swapchainImages; + private VulkanDevice _device { get; } + private ImageView[] _swapchainImageViews = new ImageView[0]; + private bool _vsyncStateChanged; + private bool _vsyncEnabled; + + public VulkanCommandBufferPool CommandBufferPool { get; set; } + + public object Lock => _device.Lock; + + private VulkanDisplay(VulkanInstance instance, VulkanDevice device, + VulkanPhysicalDevice physicalDevice, VulkanSurface surface, SwapchainKHR swapchain, + Extent2D swapchainExtent) + { + _instance = instance; + _device = device; + _physicalDevice = physicalDevice; + _swapchain = swapchain; + _swapchainExtent = swapchainExtent; + _surface = surface; + + CreateSwapchainImages(); + + _semaphorePair = new VulkanSemaphorePair(_device); + + CommandBufferPool = new VulkanCommandBufferPool(device, physicalDevice); + } + + public PixelSize Size { get; private set; } + public uint QueueFamilyIndex => _physicalDevice.QueueFamilyIndex; + + internal SurfaceFormatKHR SurfaceFormat + { + get + { + if (_surfaceFormat.Format == Format.Undefined) + { + _surfaceFormat = _surface.GetSurfaceFormat(_physicalDevice); + } + + return _surfaceFormat; + } + } + + public void Dispose() + { + _device.WaitIdle(); + _semaphorePair?.Dispose(); + DestroyCurrentImageViews(); + _swapchainExtension.DestroySwapchain(_device.InternalHandle, _swapchain, Span<AllocationCallbacks>.Empty); + CommandBufferPool.Dispose(); + } + + private static unsafe SwapchainKHR CreateSwapchain(VulkanInstance instance, VulkanDevice device, + VulkanPhysicalDevice physicalDevice, VulkanSurface surface, out Extent2D swapchainExtent, + SwapchainKHR? oldswapchain = null, bool vsyncEnabled = true) + { + if (_swapchainExtension == null) + { + instance.Api.TryGetDeviceExtension(instance.InternalHandle, device.InternalHandle, out _swapchainExtension); + } + + while (!surface.CanSurfacePresent(physicalDevice)) + { + Thread.Sleep(16); + } + + VulkanSurface.SurfaceExtension.GetPhysicalDeviceSurfaceCapabilities(physicalDevice.InternalHandle, + surface.ApiHandle, out var capabilities); + + var imageCount = capabilities.MinImageCount + 1; + if (capabilities.MaxImageCount > 0 && imageCount > capabilities.MaxImageCount) + { + imageCount = capabilities.MaxImageCount; + } + + var surfaceFormat = surface.GetSurfaceFormat(physicalDevice); + + bool supportsIdentityTransform = capabilities.SupportedTransforms.HasFlag(SurfaceTransformFlagsKHR.SurfaceTransformIdentityBitKhr); + bool isRotated = capabilities.CurrentTransform.HasFlag(SurfaceTransformFlagsKHR.SurfaceTransformRotate90BitKhr) || + capabilities.CurrentTransform.HasFlag(SurfaceTransformFlagsKHR.SurfaceTransformRotate270BitKhr); + + swapchainExtent = GetSwapchainExtent(surface, capabilities); + + CompositeAlphaFlagsKHR compositeAlphaFlags = GetSuitableCompositeAlphaFlags(capabilities); + + PresentModeKHR presentMode = GetSuitablePresentMode(physicalDevice, surface, vsyncEnabled); + + var swapchainCreateInfo = new SwapchainCreateInfoKHR + { + SType = StructureType.SwapchainCreateInfoKhr, + Surface = surface.ApiHandle, + MinImageCount = imageCount, + ImageFormat = surfaceFormat.Format, + ImageColorSpace = surfaceFormat.ColorSpace, + ImageExtent = swapchainExtent, + ImageUsage = + ImageUsageFlags.ImageUsageColorAttachmentBit | ImageUsageFlags.ImageUsageTransferDstBit, + ImageSharingMode = SharingMode.Exclusive, + ImageArrayLayers = 1, + PreTransform = supportsIdentityTransform && isRotated ? + SurfaceTransformFlagsKHR.SurfaceTransformIdentityBitKhr : + capabilities.CurrentTransform, + CompositeAlpha = compositeAlphaFlags, + PresentMode = presentMode, + Clipped = true, + OldSwapchain = oldswapchain ?? new SwapchainKHR() + }; + + _swapchainExtension.CreateSwapchain(device.InternalHandle, swapchainCreateInfo, null, out var swapchain) + .ThrowOnError(); + + if (oldswapchain != null) + { + _swapchainExtension.DestroySwapchain(device.InternalHandle, oldswapchain.Value, null); + } + + return swapchain; + } + + private static unsafe Extent2D GetSwapchainExtent(VulkanSurface surface, SurfaceCapabilitiesKHR capabilities) + { + Extent2D swapchainExtent; + if (capabilities.CurrentExtent.Width != uint.MaxValue) + { + swapchainExtent = capabilities.CurrentExtent; + } + else + { + var surfaceSize = surface.SurfaceSize; + + var width = Math.Clamp((uint)surfaceSize.Width, capabilities.MinImageExtent.Width, capabilities.MaxImageExtent.Width); + var height = Math.Clamp((uint)surfaceSize.Height, capabilities.MinImageExtent.Height, capabilities.MaxImageExtent.Height); + + swapchainExtent = new Extent2D(width, height); + } + + return swapchainExtent; + } + + private static unsafe CompositeAlphaFlagsKHR GetSuitableCompositeAlphaFlags(SurfaceCapabilitiesKHR capabilities) + { + var compositeAlphaFlags = CompositeAlphaFlagsKHR.CompositeAlphaOpaqueBitKhr; + + if (capabilities.SupportedCompositeAlpha.HasFlag(CompositeAlphaFlagsKHR.CompositeAlphaPostMultipliedBitKhr)) + { + compositeAlphaFlags = CompositeAlphaFlagsKHR.CompositeAlphaPostMultipliedBitKhr; + } + else if (capabilities.SupportedCompositeAlpha.HasFlag(CompositeAlphaFlagsKHR.CompositeAlphaPreMultipliedBitKhr)) + { + compositeAlphaFlags = CompositeAlphaFlagsKHR.CompositeAlphaPreMultipliedBitKhr; + } + + return compositeAlphaFlags; + } + + private static unsafe PresentModeKHR GetSuitablePresentMode(VulkanPhysicalDevice physicalDevice, VulkanSurface surface, bool vsyncEnabled) + { + uint presentModesCount; + + VulkanSurface.SurfaceExtension.GetPhysicalDeviceSurfacePresentModes(physicalDevice.InternalHandle, + surface.ApiHandle, + &presentModesCount, null); + + var presentModes = new PresentModeKHR[presentModesCount]; + + fixed (PresentModeKHR* pPresentModes = presentModes) + { + VulkanSurface.SurfaceExtension.GetPhysicalDeviceSurfacePresentModes(physicalDevice.InternalHandle, + surface.ApiHandle, &presentModesCount, pPresentModes); + } + + var modes = presentModes.ToList(); + var presentMode = PresentModeKHR.PresentModeFifoKhr; + + if (!vsyncEnabled && modes.Contains(PresentModeKHR.PresentModeImmediateKhr)) + { + presentMode = PresentModeKHR.PresentModeImmediateKhr; + } + else if (modes.Contains(PresentModeKHR.PresentModeMailboxKhr)) + { + presentMode = PresentModeKHR.PresentModeMailboxKhr; + } + else if (modes.Contains(PresentModeKHR.PresentModeImmediateKhr)) + { + presentMode = PresentModeKHR.PresentModeImmediateKhr; + } + + return presentMode; + } + + internal static VulkanDisplay CreateDisplay(VulkanInstance instance, VulkanDevice device, + VulkanPhysicalDevice physicalDevice, VulkanSurface surface) + { + var swapchain = CreateSwapchain(instance, device, physicalDevice, surface, out var extent, null, true); + + return new VulkanDisplay(instance, device, physicalDevice, surface, swapchain, extent); + } + + private unsafe void CreateSwapchainImages() + { + DestroyCurrentImageViews(); + + Size = new PixelSize((int)_swapchainExtent.Width, (int)_swapchainExtent.Height); + + uint imageCount = 0; + + _swapchainExtension.GetSwapchainImages(_device.InternalHandle, _swapchain, &imageCount, null); + + _swapchainImages = new Image[imageCount]; + + fixed (Image* pSwapchainImages = _swapchainImages) + { + _swapchainExtension.GetSwapchainImages(_device.InternalHandle, _swapchain, &imageCount, pSwapchainImages); + } + + _swapchainImageViews = new ImageView[imageCount]; + + var surfaceFormat = SurfaceFormat; + + for (var i = 0; i < imageCount; i++) + { + _swapchainImageViews[i] = CreateSwapchainImageView(_swapchainImages[i], surfaceFormat.Format); + } + } + + private void DestroyCurrentImageViews() + { + for (var i = 0; i < _swapchainImageViews.Length; i++) + { + _instance.Api.DestroyImageView(_device.InternalHandle, _swapchainImageViews[i], Span<AllocationCallbacks>.Empty); + } + } + + internal void ChangeVSyncMode(bool vsyncEnabled) + { + _vsyncStateChanged = true; + _vsyncEnabled = vsyncEnabled; + } + + private void Recreate() + { + _device.WaitIdle(); + _swapchain = CreateSwapchain(_instance, _device, _physicalDevice, _surface, out _swapchainExtent, _swapchain, _vsyncEnabled); + + CreateSwapchainImages(); + } + + private unsafe ImageView CreateSwapchainImageView(Image swapchainImage, Format format) + { + var componentMapping = new ComponentMapping( + ComponentSwizzle.Identity, + ComponentSwizzle.Identity, + ComponentSwizzle.Identity, + ComponentSwizzle.Identity); + + var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectColorBit, 0, 1, 0, 1); + + var imageCreateInfo = new ImageViewCreateInfo + { + SType = StructureType.ImageViewCreateInfo, + Image = swapchainImage, + ViewType = ImageViewType.ImageViewType2D, + Format = format, + Components = componentMapping, + SubresourceRange = subresourceRange + }; + + _instance.Api.CreateImageView(_device.InternalHandle, imageCreateInfo, null, out var imageView).ThrowOnError(); + return imageView; + } + + public bool EnsureSwapchainAvailable() + { + if (Size != _surface.SurfaceSize || _vsyncStateChanged) + { + _vsyncStateChanged = false; + + Recreate(); + + return false; + } + + return true; + } + + internal VulkanCommandBufferPool.VulkanCommandBuffer StartPresentation(VulkanSurfaceRenderTarget renderTarget) + { + _nextImage = 0; + while (true) + { + var acquireResult = _swapchainExtension.AcquireNextImage( + _device.InternalHandle, + _swapchain, + ulong.MaxValue, + _semaphorePair.ImageAvailableSemaphore, + new Fence(), + ref _nextImage); + + if (acquireResult == Result.ErrorOutOfDateKhr || + acquireResult == Result.SuboptimalKhr) + { + Recreate(); + } + else + { + acquireResult.ThrowOnError(); + break; + } + } + + var commandBuffer = CommandBufferPool.CreateCommandBuffer(); + commandBuffer.BeginRecording(); + + VulkanMemoryHelper.TransitionLayout(_device, commandBuffer.InternalHandle, + _swapchainImages[_nextImage], ImageLayout.Undefined, + AccessFlags.AccessNoneKhr, + ImageLayout.TransferDstOptimal, + AccessFlags.AccessTransferWriteBit, + 1); + + return commandBuffer; + } + + internal void BlitImageToCurrentImage(VulkanSurfaceRenderTarget renderTarget, CommandBuffer commandBuffer) + { + VulkanMemoryHelper.TransitionLayout(_device, commandBuffer, + renderTarget.Image.InternalHandle.Value, (ImageLayout)renderTarget.Image.CurrentLayout, + AccessFlags.AccessNoneKhr, + ImageLayout.TransferSrcOptimal, + AccessFlags.AccessTransferReadBit, + renderTarget.MipLevels); + + var srcBlitRegion = new ImageBlit + { + SrcOffsets = new ImageBlit.SrcOffsetsBuffer + { + Element0 = new Offset3D(0, 0, 0), + Element1 = new Offset3D(renderTarget.Size.Width, renderTarget.Size.Height, 1), + }, + DstOffsets = new ImageBlit.DstOffsetsBuffer + { + Element0 = new Offset3D(0, 0, 0), + Element1 = new Offset3D(Size.Width, Size.Height, 1), + }, + SrcSubresource = new ImageSubresourceLayers + { + AspectMask = ImageAspectFlags.ImageAspectColorBit, + BaseArrayLayer = 0, + LayerCount = 1, + MipLevel = 0 + }, + DstSubresource = new ImageSubresourceLayers + { + AspectMask = ImageAspectFlags.ImageAspectColorBit, + BaseArrayLayer = 0, + LayerCount = 1, + MipLevel = 0 + } + }; + + _device.Api.CmdBlitImage(commandBuffer, renderTarget.Image.InternalHandle.Value, + ImageLayout.TransferSrcOptimal, + _swapchainImages[_nextImage], + ImageLayout.TransferDstOptimal, + 1, + srcBlitRegion, + Filter.Linear); + + VulkanMemoryHelper.TransitionLayout(_device, commandBuffer, + renderTarget.Image.InternalHandle.Value, ImageLayout.TransferSrcOptimal, + AccessFlags.AccessTransferReadBit, + (ImageLayout)renderTarget.Image.CurrentLayout, + AccessFlags.AccessNoneKhr, + renderTarget.MipLevels); + } + + internal unsafe void EndPresentation(VulkanCommandBufferPool.VulkanCommandBuffer commandBuffer) + { + VulkanMemoryHelper.TransitionLayout(_device, commandBuffer.InternalHandle, + _swapchainImages[_nextImage], ImageLayout.TransferDstOptimal, + AccessFlags.AccessNoneKhr, + ImageLayout.PresentSrcKhr, + AccessFlags.AccessNoneKhr, + 1); + + commandBuffer.Submit( + stackalloc[] { _semaphorePair.ImageAvailableSemaphore }, + stackalloc[] { PipelineStageFlags.PipelineStageColorAttachmentOutputBit }, + stackalloc[] { _semaphorePair.RenderFinishedSemaphore }); + + var semaphore = _semaphorePair.RenderFinishedSemaphore; + var swapchain = _swapchain; + var nextImage = _nextImage; + + Result result; + + var presentInfo = new PresentInfoKHR + { + SType = StructureType.PresentInfoKhr, + WaitSemaphoreCount = 1, + PWaitSemaphores = &semaphore, + SwapchainCount = 1, + PSwapchains = &swapchain, + PImageIndices = &nextImage, + PResults = &result + }; + + lock (_device.Lock) + { + _swapchainExtension.QueuePresent(_device.PresentQueue.InternalHandle, presentInfo); + } + + CommandBufferPool.FreeUsedCommandBuffers(); + } + } +} diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanImage.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanImage.cs new file mode 100644 index 00000000..343ba760 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanImage.cs @@ -0,0 +1,167 @@ +using System; +using Avalonia; +using Silk.NET.Vulkan; + +namespace Ryujinx.Ava.Ui.Vulkan +{ + internal class VulkanImage : IDisposable + { + private readonly VulkanDevice _device; + private readonly VulkanPhysicalDevice _physicalDevice; + private readonly VulkanCommandBufferPool _commandBufferPool; + private ImageLayout _currentLayout; + private AccessFlags _currentAccessFlags; + private ImageUsageFlags _imageUsageFlags { get; } + private ImageView? _imageView { get; set; } + private DeviceMemory _imageMemory { get; set; } + + internal Image? InternalHandle { get; private set; } + internal Format Format { get; } + internal ImageAspectFlags AspectFlags { get; private set; } + + public ulong Handle => InternalHandle?.Handle ?? 0; + public ulong ViewHandle => _imageView?.Handle ?? 0; + public uint UsageFlags => (uint)_imageUsageFlags; + public ulong MemoryHandle => _imageMemory.Handle; + public uint MipLevels { get; private set; } + public PixelSize Size { get; } + public ulong MemorySize { get; private set; } + public uint CurrentLayout => (uint)_currentLayout; + + public VulkanImage( + VulkanDevice device, + VulkanPhysicalDevice physicalDevice, + VulkanCommandBufferPool commandBufferPool, + uint format, + PixelSize size, + uint mipLevels = 0) + { + _device = device; + _physicalDevice = physicalDevice; + _commandBufferPool = commandBufferPool; + Format = (Format)format; + Size = size; + MipLevels = mipLevels; + _imageUsageFlags = + ImageUsageFlags.ImageUsageColorAttachmentBit | ImageUsageFlags.ImageUsageTransferDstBit | + ImageUsageFlags.ImageUsageTransferSrcBit | ImageUsageFlags.ImageUsageSampledBit; + + Initialize(); + } + + public unsafe void Initialize() + { + if (!InternalHandle.HasValue) + { + MipLevels = MipLevels != 0 ? MipLevels : (uint)Math.Floor(Math.Log(Math.Max(Size.Width, Size.Height), 2)); + + var imageCreateInfo = new ImageCreateInfo + { + SType = StructureType.ImageCreateInfo, + ImageType = ImageType.ImageType2D, + Format = Format, + Extent = new Extent3D((uint?)Size.Width, (uint?)Size.Height, 1), + MipLevels = MipLevels, + ArrayLayers = 1, + Samples = SampleCountFlags.SampleCount1Bit, + Tiling = Tiling, + Usage = _imageUsageFlags, + SharingMode = SharingMode.Exclusive, + InitialLayout = ImageLayout.Undefined, + Flags = ImageCreateFlags.ImageCreateMutableFormatBit + }; + + _device.Api.CreateImage(_device.InternalHandle, imageCreateInfo, null, out var image).ThrowOnError(); + InternalHandle = image; + + _device.Api.GetImageMemoryRequirements(_device.InternalHandle, InternalHandle.Value, + out var memoryRequirements); + + var memoryAllocateInfo = new MemoryAllocateInfo + { + SType = StructureType.MemoryAllocateInfo, + AllocationSize = memoryRequirements.Size, + MemoryTypeIndex = (uint)VulkanMemoryHelper.FindSuitableMemoryTypeIndex( + _physicalDevice, + memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.MemoryPropertyDeviceLocalBit) + }; + + _device.Api.AllocateMemory(_device.InternalHandle, memoryAllocateInfo, null, + out var imageMemory); + + _imageMemory = imageMemory; + + _device.Api.BindImageMemory(_device.InternalHandle, InternalHandle.Value, _imageMemory, 0); + + MemorySize = memoryRequirements.Size; + + var componentMapping = new ComponentMapping( + ComponentSwizzle.Identity, + ComponentSwizzle.Identity, + ComponentSwizzle.Identity, + ComponentSwizzle.Identity); + + AspectFlags = ImageAspectFlags.ImageAspectColorBit; + + var subresourceRange = new ImageSubresourceRange(AspectFlags, 0, MipLevels, 0, 1); + + var imageViewCreateInfo = new ImageViewCreateInfo + { + SType = StructureType.ImageViewCreateInfo, + Image = InternalHandle.Value, + ViewType = ImageViewType.ImageViewType2D, + Format = Format, + Components = componentMapping, + SubresourceRange = subresourceRange + }; + + _device.Api + .CreateImageView(_device.InternalHandle, imageViewCreateInfo, null, out var imageView) + .ThrowOnError(); + + _imageView = imageView; + + _currentLayout = ImageLayout.Undefined; + + TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr); + } + } + + public ImageTiling Tiling => ImageTiling.Optimal; + + internal void TransitionLayout(ImageLayout destinationLayout, AccessFlags destinationAccessFlags) + { + var commandBuffer = _commandBufferPool.CreateCommandBuffer(); + commandBuffer.BeginRecording(); + + VulkanMemoryHelper.TransitionLayout(_device, commandBuffer.InternalHandle, InternalHandle.Value, + _currentLayout, + _currentAccessFlags, + destinationLayout, destinationAccessFlags, + MipLevels); + + commandBuffer.EndRecording(); + + commandBuffer.Submit(); + + _currentLayout = destinationLayout; + _currentAccessFlags = destinationAccessFlags; + } + + public void TransitionLayout(uint destinationLayout, uint destinationAccessFlags) + { + TransitionLayout((ImageLayout)destinationLayout, (AccessFlags)destinationAccessFlags); + } + + public unsafe void Dispose() + { + _device.Api.DestroyImageView(_device.InternalHandle, _imageView.Value, null); + _device.Api.DestroyImage(_device.InternalHandle, InternalHandle.Value, null); + _device.Api.FreeMemory(_device.InternalHandle, _imageMemory, null); + + _imageView = default; + InternalHandle = default; + _imageMemory = default; + } + } +} diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanInstance.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanInstance.cs new file mode 100644 index 00000000..a3a9ea61 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanInstance.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using Silk.NET.Core; +using Silk.NET.Vulkan; +using Silk.NET.Vulkan.Extensions.EXT; + +namespace Ryujinx.Ava.Ui.Vulkan +{ + public class VulkanInstance : IDisposable + { + private const string EngineName = "Avalonia Vulkan"; + + private VulkanInstance(Instance apiHandle, Vk api) + { + InternalHandle = apiHandle; + Api = api; + } + + public IntPtr Handle => InternalHandle.Handle; + + internal Instance InternalHandle { get; } + public Vk Api { get; } + + internal static IEnumerable<string> RequiredInstanceExtensions + { + get + { + yield return "VK_KHR_surface"; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + yield return "VK_KHR_xlib_surface"; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + yield return "VK_KHR_win32_surface"; + } + } + } + + public void Dispose() + { + Api?.DestroyInstance(InternalHandle, Span<AllocationCallbacks>.Empty); + Api?.Dispose(); + } + + internal static unsafe VulkanInstance Create(VulkanOptions options) + { + var api = Vk.GetApi(); + var applicationName = Marshal.StringToHGlobalAnsi(options.ApplicationName); + var engineName = Marshal.StringToHGlobalAnsi(EngineName); + var enabledExtensions = new List<string>(options.InstanceExtensions); + + enabledExtensions.AddRange(RequiredInstanceExtensions); + + var applicationInfo = new ApplicationInfo + { + PApplicationName = (byte*)applicationName, + ApiVersion = new Version32((uint)options.VulkanVersion.Major, (uint)options.VulkanVersion.Minor, + (uint)options.VulkanVersion.Build), + PEngineName = (byte*)engineName, + EngineVersion = new Version32(1, 0, 0), + ApplicationVersion = new Version32(1, 0, 0) + }; + + var enabledLayers = new HashSet<string>(); + + if (options.UseDebug) + { + enabledExtensions.Add(ExtDebugUtils.ExtensionName); + enabledExtensions.Add(ExtDebugReport.ExtensionName); + if (IsLayerAvailable(api, "VK_LAYER_KHRONOS_validation")) + enabledLayers.Add("VK_LAYER_KHRONOS_validation"); + } + + foreach (var layer in options.EnabledLayers) + enabledLayers.Add(layer); + + var ppEnabledExtensions = stackalloc IntPtr[enabledExtensions.Count]; + var ppEnabledLayers = stackalloc IntPtr[enabledLayers.Count]; + + for (var i = 0; i < enabledExtensions.Count; i++) + ppEnabledExtensions[i] = Marshal.StringToHGlobalAnsi(enabledExtensions[i]); + + var layers = enabledLayers.ToList(); + + for (var i = 0; i < enabledLayers.Count; i++) + ppEnabledLayers[i] = Marshal.StringToHGlobalAnsi(layers[i]); + + var instanceCreateInfo = new InstanceCreateInfo + { + SType = StructureType.InstanceCreateInfo, + PApplicationInfo = &applicationInfo, + PpEnabledExtensionNames = (byte**)ppEnabledExtensions, + PpEnabledLayerNames = (byte**)ppEnabledLayers, + EnabledExtensionCount = (uint)enabledExtensions.Count, + EnabledLayerCount = (uint)enabledLayers.Count + }; + + api.CreateInstance(in instanceCreateInfo, null, out var instance).ThrowOnError(); + + Marshal.FreeHGlobal(applicationName); + Marshal.FreeHGlobal(engineName); + + for (var i = 0; i < enabledExtensions.Count; i++) Marshal.FreeHGlobal(ppEnabledExtensions[i]); + + for (var i = 0; i < enabledLayers.Count; i++) Marshal.FreeHGlobal(ppEnabledLayers[i]); + + return new VulkanInstance(instance, api); + } + + private static unsafe bool IsLayerAvailable(Vk api, string layerName) + { + uint layerPropertiesCount; + + api.EnumerateInstanceLayerProperties(&layerPropertiesCount, null).ThrowOnError(); + + var layerProperties = new LayerProperties[layerPropertiesCount]; + + fixed (LayerProperties* pLayerProperties = layerProperties) + { + api.EnumerateInstanceLayerProperties(&layerPropertiesCount, layerProperties).ThrowOnError(); + + for (var i = 0; i < layerPropertiesCount; i++) + { + var currentLayerName = Marshal.PtrToStringAnsi((IntPtr)pLayerProperties[i].LayerName); + + if (currentLayerName == layerName) return true; + } + } + + return false; + } + } +} diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanMemoryHelper.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanMemoryHelper.cs new file mode 100644 index 00000000..a7052592 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanMemoryHelper.cs @@ -0,0 +1,59 @@ +using Silk.NET.Vulkan; + +namespace Ryujinx.Ava.Ui.Vulkan +{ + internal static class VulkanMemoryHelper + { + internal static int FindSuitableMemoryTypeIndex(VulkanPhysicalDevice physicalDevice, uint memoryTypeBits, + MemoryPropertyFlags flags) + { + physicalDevice.Api.GetPhysicalDeviceMemoryProperties(physicalDevice.InternalHandle, out var properties); + + for (var i = 0; i < properties.MemoryTypeCount; i++) + { + var type = properties.MemoryTypes[i]; + + if ((memoryTypeBits & (1 << i)) != 0 && type.PropertyFlags.HasFlag(flags)) return i; + } + + return -1; + } + + internal static unsafe void TransitionLayout(VulkanDevice device, + CommandBuffer commandBuffer, + Image image, + ImageLayout sourceLayout, + AccessFlags sourceAccessMask, + ImageLayout destinationLayout, + AccessFlags destinationAccessMask, + uint mipLevels) + { + var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectColorBit, 0, mipLevels, 0, 1); + + var barrier = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = sourceAccessMask, + DstAccessMask = destinationAccessMask, + OldLayout = sourceLayout, + NewLayout = destinationLayout, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = image, + SubresourceRange = subresourceRange + }; + + device.Api.CmdPipelineBarrier( + commandBuffer, + PipelineStageFlags.PipelineStageAllCommandsBit, + PipelineStageFlags.PipelineStageAllCommandsBit, + 0, + 0, + null, + 0, + null, + 1, + barrier); + } + } +} diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanOptions.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanOptions.cs new file mode 100644 index 00000000..8e836398 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanOptions.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Ryujinx.Ava.Ui.Vulkan +{ + public class VulkanOptions + { + /// <summary> + /// Sets the application name of the Vulkan instance + /// </summary> + public string ApplicationName { get; set; } + + /// <summary> + /// Specifies the Vulkan API version to use + /// </summary> + public Version VulkanVersion { get; set; } = new Version(1, 1, 0); + + /// <summary> + /// Specifies additional extensions to enable if available on the instance + /// </summary> + public IEnumerable<string> InstanceExtensions { get; set; } = Enumerable.Empty<string>(); + + /// <summary> + /// Specifies layers to enable if available on the instance + /// </summary> + public IEnumerable<string> EnabledLayers { get; set; } = Enumerable.Empty<string>(); + + /// <summary> + /// Enables the debug layer + /// </summary> + public bool UseDebug { get; set; } + + /// <summary> + /// Selects the first suitable discrete GPU available + /// </summary> + public bool PreferDiscreteGpu { get; set; } + + /// <summary> + /// Sets the device to use if available and suitable. + /// </summary> + public string PreferredDevice { get; set; } + + /// <summary> + /// Max number of device queues to request + /// </summary> + public uint MaxQueueCount { get; set; } + } +} diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPhysicalDevice.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPhysicalDevice.cs new file mode 100644 index 00000000..11444d30 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPhysicalDevice.cs @@ -0,0 +1,219 @@ +using Ryujinx.Graphics.Vulkan; +using Silk.NET.Core; +using Silk.NET.Vulkan; +using Silk.NET.Vulkan.Extensions.KHR; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; + +namespace Ryujinx.Ava.Ui.Vulkan +{ + public unsafe class VulkanPhysicalDevice + { + private VulkanPhysicalDevice(PhysicalDevice apiHandle, Vk api, uint queueCount, uint queueFamilyIndex) + { + InternalHandle = apiHandle; + Api = api; + QueueCount = queueCount; + QueueFamilyIndex = queueFamilyIndex; + + api.GetPhysicalDeviceProperties(apiHandle, out var properties); + + DeviceName = Marshal.PtrToStringAnsi((IntPtr)properties.DeviceName); + DeviceId = VulkanInitialization.StringFromIdPair(properties.VendorID, properties.DeviceID); + + var version = (Version32)properties.ApiVersion; + ApiVersion = new Version((int)version.Major, (int)version.Minor, 0, (int)version.Patch); + } + + internal PhysicalDevice InternalHandle { get; } + internal Vk Api { get; } + public uint QueueCount { get; } + public uint QueueFamilyIndex { get; } + public IntPtr Handle => InternalHandle.Handle; + + public string DeviceName { get; } + public string DeviceId { get; } + public Version ApiVersion { get; } + public static Dictionary<PhysicalDevice, PhysicalDeviceProperties> PhysicalDevices { get; private set; } + public static IEnumerable<KeyValuePair<PhysicalDevice, PhysicalDeviceProperties>> SuitableDevices { get; private set; } + + internal static void SelectAvailableDevices(VulkanInstance instance, + VulkanSurface surface, bool preferDiscreteGpu, string preferredDevice) + { + uint physicalDeviceCount; + + instance.Api.EnumeratePhysicalDevices(instance.InternalHandle, &physicalDeviceCount, null).ThrowOnError(); + + var physicalDevices = new PhysicalDevice[physicalDeviceCount]; + + fixed (PhysicalDevice* pPhysicalDevices = physicalDevices) + { + instance.Api.EnumeratePhysicalDevices(instance.InternalHandle, &physicalDeviceCount, pPhysicalDevices) + .ThrowOnError(); + } + + PhysicalDevices = new Dictionary<PhysicalDevice, PhysicalDeviceProperties>(); + + foreach (var physicalDevice in physicalDevices) + { + instance.Api.GetPhysicalDeviceProperties(physicalDevice, out var properties); + PhysicalDevices.Add(physicalDevice, properties); + } + + SuitableDevices = PhysicalDevices.Where(x => IsSuitableDevice( + instance.Api, + x.Key, + x.Value, + surface.ApiHandle, + out _, + out _)); + } + + internal static VulkanPhysicalDevice FindSuitablePhysicalDevice(VulkanInstance instance, + VulkanSurface surface, bool preferDiscreteGpu, string preferredDevice) + { + SelectAvailableDevices(instance, surface, preferDiscreteGpu, preferredDevice); + + uint queueFamilyIndex = 0; + uint queueCount = 0; + + if (!string.IsNullOrWhiteSpace(preferredDevice)) + { + var physicalDevice = SuitableDevices.FirstOrDefault(x => VulkanInitialization.StringFromIdPair(x.Value.VendorID, x.Value.DeviceID) == preferredDevice); + + queueFamilyIndex = FindSuitableQueueFamily(instance.Api, physicalDevice.Key, + surface.ApiHandle, out queueCount); + if (queueFamilyIndex != int.MaxValue) + { + return new VulkanPhysicalDevice(physicalDevice.Key, instance.Api, queueCount, queueFamilyIndex); + } + } + + if (preferDiscreteGpu) + { + var discreteGpus = SuitableDevices.Where(p => p.Value.DeviceType == PhysicalDeviceType.DiscreteGpu); + + foreach (var gpu in discreteGpus) + { + queueFamilyIndex = FindSuitableQueueFamily(instance.Api, gpu.Key, + surface.ApiHandle, out queueCount); + if (queueFamilyIndex != int.MaxValue) + { + return new VulkanPhysicalDevice(gpu.Key, instance.Api, queueCount, queueFamilyIndex); + } + } + } + + foreach (var physicalDevice in SuitableDevices) + { + queueFamilyIndex = FindSuitableQueueFamily(instance.Api, physicalDevice.Key, + surface.ApiHandle, out queueCount); + if (queueFamilyIndex != int.MaxValue) + { + return new VulkanPhysicalDevice(physicalDevice.Key, instance.Api, queueCount, queueFamilyIndex); + } + } + + throw new Exception("No suitable physical device found"); + } + + private static unsafe bool IsSuitableDevice(Vk api, PhysicalDevice physicalDevice, PhysicalDeviceProperties properties, SurfaceKHR surface, + out uint queueCount, out uint familyIndex) + { + queueCount = 0; + familyIndex = 0; + + if (properties.DeviceType == PhysicalDeviceType.Cpu) return false; + + var extensionMatches = 0; + uint propertiesCount; + + api.EnumerateDeviceExtensionProperties(physicalDevice, (byte*)null, &propertiesCount, null).ThrowOnError(); + + var extensionProperties = new ExtensionProperties[propertiesCount]; + + fixed (ExtensionProperties* pExtensionProperties = extensionProperties) + { + api.EnumerateDeviceExtensionProperties( + physicalDevice, + (byte*)null, + &propertiesCount, + pExtensionProperties).ThrowOnError(); + + for (var i = 0; i < propertiesCount; i++) + { + var extensionName = Marshal.PtrToStringAnsi((IntPtr)pExtensionProperties[i].ExtensionName); + + if (VulkanInitialization.RequiredExtensions.Contains(extensionName)) + { + extensionMatches++; + } + } + } + + if (extensionMatches == VulkanInitialization.RequiredExtensions.Length) + { + familyIndex = FindSuitableQueueFamily(api, physicalDevice, surface, out queueCount); + + return familyIndex != uint.MaxValue; + } + + return false; + } + + internal unsafe string[] GetSupportedExtensions() + { + uint propertiesCount; + + Api.EnumerateDeviceExtensionProperties(InternalHandle, (byte*)null, &propertiesCount, null).ThrowOnError(); + + var extensionProperties = new ExtensionProperties[propertiesCount]; + + fixed (ExtensionProperties* pExtensionProperties = extensionProperties) + { + Api.EnumerateDeviceExtensionProperties(InternalHandle, (byte*)null, &propertiesCount, pExtensionProperties) + .ThrowOnError(); + } + + return extensionProperties.Select(x => Marshal.PtrToStringAnsi((IntPtr)x.ExtensionName)).ToArray(); + } + + private static uint FindSuitableQueueFamily(Vk api, PhysicalDevice physicalDevice, SurfaceKHR surface, + out uint queueCount) + { + const QueueFlags RequiredFlags = QueueFlags.QueueGraphicsBit | QueueFlags.QueueComputeBit; + + var khrSurface = new KhrSurface(api.Context); + + uint propertiesCount; + + api.GetPhysicalDeviceQueueFamilyProperties(physicalDevice, &propertiesCount, null); + + var properties = new QueueFamilyProperties[propertiesCount]; + + fixed (QueueFamilyProperties* pProperties = properties) + { + api.GetPhysicalDeviceQueueFamilyProperties(physicalDevice, &propertiesCount, pProperties); + } + + for (uint index = 0; index < propertiesCount; index++) + { + var queueFlags = properties[index].QueueFlags; + + khrSurface.GetPhysicalDeviceSurfaceSupport(physicalDevice, index, surface, out var surfaceSupported) + .ThrowOnError(); + + if (queueFlags.HasFlag(RequiredFlags) && surfaceSupported) + { + queueCount = properties[index].QueueCount; + return index; + } + } + + queueCount = 0; + return uint.MaxValue; + } + } +} diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPlatformInterface.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPlatformInterface.cs new file mode 100644 index 00000000..47a07949 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanPlatformInterface.cs @@ -0,0 +1,80 @@ +using Avalonia; +using Ryujinx.Ava.Ui.Vulkan.Surfaces; +using Ryujinx.Graphics.Vulkan; +using Silk.NET.Vulkan; +using System; + +namespace Ryujinx.Ava.Ui.Vulkan +{ + internal class VulkanPlatformInterface : IDisposable + { + private static VulkanOptions _options; + + private VulkanPlatformInterface(VulkanInstance instance) + { + Instance = instance; + Api = instance.Api; + } + + public VulkanPhysicalDevice PhysicalDevice { get; private set; } + public VulkanInstance Instance { get; } + public VulkanDevice Device { get; set; } + public Vk Api { get; private set; } + public VulkanSurfaceRenderTarget MainSurface { get; set; } + + public void Dispose() + { + Device?.Dispose(); + Instance?.Dispose(); + Api?.Dispose(); + } + + private static VulkanPlatformInterface TryCreate() + { + _options = AvaloniaLocator.Current.GetService<VulkanOptions>() ?? new VulkanOptions(); + + var instance = VulkanInstance.Create(_options); + + return new VulkanPlatformInterface(instance); + } + + public static bool TryInitialize() + { + var feature = TryCreate(); + if (feature != null) + { + AvaloniaLocator.CurrentMutable.Bind<VulkanPlatformInterface>().ToConstant(feature); + return true; + } + + return false; + } + + public VulkanSurfaceRenderTarget CreateRenderTarget(IVulkanPlatformSurface platformSurface) + { + var surface = VulkanSurface.CreateSurface(Instance, platformSurface); + + if (Device == null) + { + PhysicalDevice = VulkanPhysicalDevice.FindSuitablePhysicalDevice(Instance, surface, _options.PreferDiscreteGpu, _options.PreferredDevice); + var device = VulkanInitialization.CreateDevice(Instance.Api, + PhysicalDevice.InternalHandle, + PhysicalDevice.QueueFamilyIndex, + VulkanInitialization.GetSupportedExtensions(Instance.Api, PhysicalDevice.InternalHandle), + PhysicalDevice.QueueCount); + + Device = new VulkanDevice(device, PhysicalDevice, Instance.Api); + } + + var renderTarget = new VulkanSurfaceRenderTarget(this, surface); + + if (MainSurface == null && surface != null) + { + MainSurface = renderTarget; + MainSurface.Display.ChangeVSyncMode(false); + } + + return renderTarget; + } + } +} diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanQueue.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanQueue.cs new file mode 100644 index 00000000..a903e21a --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanQueue.cs @@ -0,0 +1,18 @@ +using System; +using Silk.NET.Vulkan; + +namespace Ryujinx.Ava.Ui.Vulkan +{ + internal class VulkanQueue + { + public VulkanQueue(VulkanDevice device, Queue apiHandle) + { + Device = device; + InternalHandle = apiHandle; + } + + public VulkanDevice Device { get; } + public IntPtr Handle => InternalHandle.Handle; + internal Queue InternalHandle { get; } + } +} diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSemaphorePair.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSemaphorePair.cs new file mode 100644 index 00000000..3b5fd9cc --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSemaphorePair.cs @@ -0,0 +1,32 @@ +using System; +using Silk.NET.Vulkan; + +namespace Ryujinx.Ava.Ui.Vulkan +{ + internal class VulkanSemaphorePair : IDisposable + { + private readonly VulkanDevice _device; + + public unsafe VulkanSemaphorePair(VulkanDevice device) + { + _device = device; + + var semaphoreCreateInfo = new SemaphoreCreateInfo { SType = StructureType.SemaphoreCreateInfo }; + + _device.Api.CreateSemaphore(_device.InternalHandle, semaphoreCreateInfo, null, out var semaphore).ThrowOnError(); + ImageAvailableSemaphore = semaphore; + + _device.Api.CreateSemaphore(_device.InternalHandle, semaphoreCreateInfo, null, out semaphore).ThrowOnError(); + RenderFinishedSemaphore = semaphore; + } + + internal Semaphore ImageAvailableSemaphore { get; } + internal Semaphore RenderFinishedSemaphore { get; } + + public unsafe void Dispose() + { + _device.Api.DestroySemaphore(_device.InternalHandle, ImageAvailableSemaphore, null); + _device.Api.DestroySemaphore(_device.InternalHandle, RenderFinishedSemaphore, null); + } + } +} diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurface.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurface.cs new file mode 100644 index 00000000..2452cdcd --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurface.cs @@ -0,0 +1,75 @@ +using System; +using Avalonia; +using Ryujinx.Ava.Ui.Vulkan.Surfaces; +using Silk.NET.Vulkan; +using Silk.NET.Vulkan.Extensions.KHR; + +namespace Ryujinx.Ava.Ui.Vulkan +{ + public class VulkanSurface : IDisposable + { + private readonly VulkanInstance _instance; + private readonly IVulkanPlatformSurface _vulkanPlatformSurface; + + private VulkanSurface(IVulkanPlatformSurface vulkanPlatformSurface, VulkanInstance instance) + { + _vulkanPlatformSurface = vulkanPlatformSurface; + _instance = instance; + ApiHandle = vulkanPlatformSurface.CreateSurface(instance); + } + + internal SurfaceKHR ApiHandle { get; } + + internal static KhrSurface SurfaceExtension { get; private set; } + + internal PixelSize SurfaceSize => _vulkanPlatformSurface.SurfaceSize; + + public void Dispose() + { + SurfaceExtension.DestroySurface(_instance.InternalHandle, ApiHandle, Span<AllocationCallbacks>.Empty); + _vulkanPlatformSurface.Dispose(); + } + + internal static VulkanSurface CreateSurface(VulkanInstance instance, IVulkanPlatformSurface vulkanPlatformSurface) + { + if (SurfaceExtension == null) + { + instance.Api.TryGetInstanceExtension(instance.InternalHandle, out KhrSurface extension); + + SurfaceExtension = extension; + } + + return new VulkanSurface(vulkanPlatformSurface, instance); + } + + internal bool CanSurfacePresent(VulkanPhysicalDevice physicalDevice) + { + SurfaceExtension.GetPhysicalDeviceSurfaceSupport(physicalDevice.InternalHandle, physicalDevice.QueueFamilyIndex, ApiHandle, out var isSupported); + + return isSupported; + } + + internal SurfaceFormatKHR GetSurfaceFormat(VulkanPhysicalDevice physicalDevice) + { + Span<uint> surfaceFormatsCount = stackalloc uint[1]; + SurfaceExtension.GetPhysicalDeviceSurfaceFormats(physicalDevice.InternalHandle, ApiHandle, surfaceFormatsCount, Span<SurfaceFormatKHR>.Empty); + Span<SurfaceFormatKHR> surfaceFormats = stackalloc SurfaceFormatKHR[(int)surfaceFormatsCount[0]]; + SurfaceExtension.GetPhysicalDeviceSurfaceFormats(physicalDevice.InternalHandle, ApiHandle, surfaceFormatsCount, surfaceFormats); + + if (surfaceFormats.Length == 1 && surfaceFormats[0].Format == Format.Undefined) + { + return new SurfaceFormatKHR(Format.B8G8R8A8Unorm, ColorSpaceKHR.ColorspaceSrgbNonlinearKhr); + } + + foreach (var format in surfaceFormats) + { + if (format.Format == Format.B8G8R8A8Unorm && format.ColorSpace == ColorSpaceKHR.ColorspaceSrgbNonlinearKhr) + { + return format; + } + } + + return surfaceFormats[0]; + } + } +} diff --git a/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurfaceRenderingSession.cs b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurfaceRenderingSession.cs new file mode 100644 index 00000000..8833ede5 --- /dev/null +++ b/Ryujinx.Ava/Ui/Backend/Vulkan/VulkanSurfaceRenderingSession.cs @@ -0,0 +1,48 @@ +using System; +using Avalonia; +using Ryujinx.Ava.Ui.Vulkan.Surfaces; +using Silk.NET.Vulkan; + +namespace Ryujinx.Ava.Ui.Vulkan +{ + internal class VulkanSurfaceRenderingSession : IDisposable + { + private readonly VulkanDevice _device; + private readonly VulkanSurfaceRenderTarget _renderTarget; + private VulkanCommandBufferPool.VulkanCommandBuffer _commandBuffer; + + public VulkanSurfaceRenderingSession(VulkanDisplay display, VulkanDevice device, + VulkanSurfaceRenderTarget renderTarget, float scaling) + { + Display = display; + _device = device; + _renderTarget = renderTarget; + Scaling = scaling; + Begin(); + } + + public VulkanDisplay Display { get; } + + public PixelSize Size => _renderTarget.Size; + public Vk Api => _device.Api; + + public float Scaling { get; } + + private void Begin() + { + if (!Display.EnsureSwapchainAvailable()) + { + _renderTarget.Invalidate(); + } + } + + public void Dispose() + { + _commandBuffer = Display.StartPresentation(_renderTarget); + + Display.BlitImageToCurrentImage(_renderTarget, _commandBuffer.InternalHandle); + + Display.EndPresentation(_commandBuffer); + } + } +} diff --git a/Ryujinx.Ava/Ui/Controls/OpenGLRendererControl.cs b/Ryujinx.Ava/Ui/Controls/OpenGLRendererControl.cs new file mode 100644 index 00000000..db9caca1 --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/OpenGLRendererControl.cs @@ -0,0 +1,190 @@ +using Avalonia; +using Avalonia.OpenGL; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Skia; +using Avalonia.Threading; +using OpenTK.Graphics.OpenGL; +using Ryujinx.Common.Configuration; +using SkiaSharp; +using SPB.Graphics; +using SPB.Graphics.OpenGL; +using SPB.Platform; +using SPB.Windowing; +using System; + +namespace Ryujinx.Ava.Ui.Controls +{ + internal class OpenGLRendererControl : RendererControl + { + public int Major { get; } + public int Minor { get; } + public OpenGLContextBase GameContext { get; set; } + + public static OpenGLContextBase PrimaryContext => + AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>() + .PrimaryContext.AsOpenGLContextBase(); + + private SwappableNativeWindowBase _gameBackgroundWindow; + + private IntPtr _fence; + + public OpenGLRendererControl(int major, int minor, GraphicsDebugLevel graphicsDebugLevel) : base(graphicsDebugLevel) + { + Major = major; + Minor = minor; + } + + public override void DestroyBackgroundContext() + { + _image = null; + + if (_fence != IntPtr.Zero) + { + DrawOperation.Dispose(); + GL.DeleteSync(_fence); + } + + GlDrawOperation.DeleteFramebuffer(); + + GameContext?.Dispose(); + + _gameBackgroundWindow?.Dispose(); + } + + internal override void Present(object image) + { + Dispatcher.UIThread.InvokeAsync(() => + { + Image = (int)image; + }).Wait(); + + if (_fence != IntPtr.Zero) + { + GL.DeleteSync(_fence); + } + + _fence = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None); + + QueueRender(); + + _gameBackgroundWindow.SwapBuffers(); + } + + internal override void MakeCurrent() + { + GameContext.MakeCurrent(_gameBackgroundWindow); + } + + internal override void MakeCurrent(SwappableNativeWindowBase window) + { + GameContext.MakeCurrent(window); + } + + protected override void CreateWindow() + { + var flags = OpenGLContextFlags.Compat; + if (DebugLevel != GraphicsDebugLevel.None) + { + flags |= OpenGLContextFlags.Debug; + } + _gameBackgroundWindow = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100); + _gameBackgroundWindow.Hide(); + + GameContext = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, Major, Minor, flags, shareContext: PrimaryContext); + GameContext.Initialize(_gameBackgroundWindow); + } + + protected override ICustomDrawOperation CreateDrawOperation() + { + return new GlDrawOperation(this); + } + + private class GlDrawOperation : ICustomDrawOperation + { + private static int _framebuffer; + + public Rect Bounds { get; } + + private readonly OpenGLRendererControl _control; + + public GlDrawOperation(OpenGLRendererControl control) + { + _control = control; + Bounds = _control.Bounds; + } + + public void Dispose() { } + + public static void DeleteFramebuffer() + { + if (_framebuffer == 0) + { + GL.DeleteFramebuffer(_framebuffer); + } + + _framebuffer = 0; + } + + public bool Equals(ICustomDrawOperation other) + { + return other is GlDrawOperation operation && Equals(this, operation) && operation.Bounds == Bounds; + } + + public bool HitTest(Point p) + { + return Bounds.Contains(p); + } + + private void CreateRenderTarget() + { + _framebuffer = GL.GenFramebuffer(); + } + + public void Render(IDrawingContextImpl context) + { + if (_control.Image == null) + { + return; + } + + if (_framebuffer == 0) + { + CreateRenderTarget(); + } + + int currentFramebuffer = GL.GetInteger(GetPName.FramebufferBinding); + + var image = _control.Image; + var fence = _control._fence; + + GL.BindFramebuffer(FramebufferTarget.Framebuffer, _framebuffer); + GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, (int)image, 0); + GL.BindFramebuffer(FramebufferTarget.Framebuffer, currentFramebuffer); + + if (context is not ISkiaDrawingContextImpl skiaDrawingContextImpl) + { + return; + } + + var imageInfo = new SKImageInfo((int)_control.RenderSize.Width, (int)_control.RenderSize.Height, SKColorType.Rgba8888); + var glInfo = new GRGlFramebufferInfo((uint)_framebuffer, SKColorType.Rgba8888.ToGlSizedFormat()); + + GL.WaitSync(fence, WaitSyncFlags.None, ulong.MaxValue); + + using var backendTexture = new GRBackendRenderTarget(imageInfo.Width, imageInfo.Height, 1, 0, glInfo); + using var surface = SKSurface.Create(skiaDrawingContextImpl.GrContext, backendTexture, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888); + + if (surface == null) + { + return; + } + + var rect = new Rect(new Point(), _control.RenderSize); + + using var snapshot = surface.Snapshot(); + skiaDrawingContextImpl.SkCanvas.DrawImage(snapshot, rect.ToSKRect(), _control.Bounds.ToSKRect(), new SKPaint()); + } + } + } +} diff --git a/Ryujinx.Ava/Ui/Controls/RendererControl.cs b/Ryujinx.Ava/Ui/Controls/RendererControl.cs index f81d7e17..130348f2 100644 --- a/Ryujinx.Ava/Ui/Controls/RendererControl.cs +++ b/Ryujinx.Ava/Ui/Controls/RendererControl.cs @@ -2,65 +2,49 @@ using Avalonia.Controls; using Avalonia.Data; using Avalonia.Media; -using Avalonia.OpenGL; -using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; -using Avalonia.Skia; -using Avalonia.Threading; -using OpenTK.Graphics.OpenGL; using Ryujinx.Common.Configuration; -using SkiaSharp; -using SPB.Graphics; -using SPB.Graphics.OpenGL; -using SPB.Platform; using SPB.Windowing; using System; namespace Ryujinx.Ava.Ui.Controls { - internal class RendererControl : Control + internal abstract class RendererControl : Control { - private int _image; + protected object _image; static RendererControl() { AffectsRender<RendererControl>(ImageProperty); } - public readonly static StyledProperty<int> ImageProperty = - AvaloniaProperty.Register<RendererControl, int>(nameof(Image), 0, inherits: true, defaultBindingMode: BindingMode.TwoWay); + public readonly static StyledProperty<object> ImageProperty = + AvaloniaProperty.Register<RendererControl, object>( + nameof(Image), + 0, + inherits: true, + defaultBindingMode: BindingMode.TwoWay); - protected int Image + protected object Image { get => _image; set => SetAndRaise(ImageProperty, ref _image, value); } - public event EventHandler<EventArgs> GlInitialized; + public event EventHandler<EventArgs> RendererInitialized; public event EventHandler<Size> SizeChanged; protected Size RenderSize { get; private set; } public bool IsStarted { get; private set; } - public int Major { get; } - public int Minor { get; } public GraphicsDebugLevel DebugLevel { get; } - public OpenGLContextBase GameContext { get; set; } - - public static OpenGLContextBase PrimaryContext => AvaloniaLocator.Current.GetService<IPlatformOpenGlInterface>().PrimaryContext.AsOpenGLContextBase(); - - private SwappableNativeWindowBase _gameBackgroundWindow; private bool _isInitialized; - private IntPtr _fence; - - private GlDrawOperation _glDrawOperation; + protected ICustomDrawOperation DrawOperation { get; private set; } - public RendererControl(int major, int minor, GraphicsDebugLevel graphicsDebugLevel) + public RendererControl(GraphicsDebugLevel graphicsDebugLevel) { - Major = major; - Minor = minor; DebugLevel = graphicsDebugLevel; IObservable<Rect> resizeObservable = this.GetObservable(BoundsProperty); @@ -69,7 +53,7 @@ namespace Ryujinx.Ava.Ui.Controls Focusable = true; } - private void Resized(Rect rect) + protected void Resized(Rect rect) { SizeChanged?.Invoke(this, rect.Size); @@ -77,37 +61,40 @@ namespace Ryujinx.Ava.Ui.Controls { RenderSize = rect.Size * VisualRoot.RenderScaling; - _glDrawOperation?.Dispose(); - _glDrawOperation = new GlDrawOperation(this); + DrawOperation?.Dispose(); + DrawOperation = CreateDrawOperation(); } } + protected abstract ICustomDrawOperation CreateDrawOperation(); + protected abstract void CreateWindow(); + public override void Render(DrawingContext context) { if (!_isInitialized) { CreateWindow(); - OnGlInitialized(); + OnRendererInitialized(); _isInitialized = true; } - if (GameContext == null || !IsStarted || Image == 0) + if (!IsStarted || Image == null) { return; } - if (_glDrawOperation != null) + if (DrawOperation != null) { - context.Custom(_glDrawOperation); + context.Custom(DrawOperation); } base.Render(context); } - protected void OnGlInitialized() + protected void OnRendererInitialized() { - GlInitialized?.Invoke(this, EventArgs.Empty); + RendererInitialized?.Invoke(this, EventArgs.Empty); } public void QueueRender() @@ -115,24 +102,7 @@ namespace Ryujinx.Ava.Ui.Controls Program.RenderTimer.TickNow(); } - internal void Present(object image) - { - Dispatcher.UIThread.InvokeAsync(() => - { - Image = (int)image; - }).Wait(); - - if (_fence != IntPtr.Zero) - { - GL.DeleteSync(_fence); - } - - _fence = GL.FenceSync(SyncCondition.SyncGpuCommandsComplete, WaitSyncFlags.None); - - QueueRender(); - - _gameBackgroundWindow.SwapBuffers(); - } + internal abstract void Present(object image); internal void Start() { @@ -145,132 +115,8 @@ namespace Ryujinx.Ava.Ui.Controls IsStarted = false; } - public void DestroyBackgroundContext() - { - _image = 0; - - if (_fence != IntPtr.Zero) - { - _glDrawOperation.Dispose(); - GL.DeleteSync(_fence); - } - - GlDrawOperation.DeleteFramebuffer(); - - GameContext?.Dispose(); - - _gameBackgroundWindow?.Dispose(); - } - - internal void MakeCurrent() - { - GameContext.MakeCurrent(_gameBackgroundWindow); - } - - internal void MakeCurrent(SwappableNativeWindowBase window) - { - GameContext.MakeCurrent(window); - } - - protected void CreateWindow() - { - var flags = OpenGLContextFlags.Compat; - if (DebugLevel != GraphicsDebugLevel.None) - { - flags |= OpenGLContextFlags.Debug; - } - _gameBackgroundWindow = PlatformHelper.CreateOpenGLWindow(FramebufferFormat.Default, 0, 0, 100, 100); - _gameBackgroundWindow.Hide(); - - GameContext = PlatformHelper.CreateOpenGLContext(FramebufferFormat.Default, Major, Minor, flags, shareContext: PrimaryContext); - GameContext.Initialize(_gameBackgroundWindow); - } - - private class GlDrawOperation : ICustomDrawOperation - { - private static int _framebuffer; - - public Rect Bounds { get; } - - private readonly RendererControl _control; - - public GlDrawOperation(RendererControl control) - { - _control = control; - Bounds = _control.Bounds; - } - - public void Dispose() { } - - public static void DeleteFramebuffer() - { - if (_framebuffer == 0) - { - GL.DeleteFramebuffer(_framebuffer); - } - - _framebuffer = 0; - } - - public bool Equals(ICustomDrawOperation other) - { - return other is GlDrawOperation operation && Equals(this, operation) && operation.Bounds == Bounds; - } - - public bool HitTest(Point p) - { - return Bounds.Contains(p); - } - - private void CreateRenderTarget() - { - _framebuffer = GL.GenFramebuffer(); - } - - public void Render(IDrawingContextImpl context) - { - if (_control.Image == 0) - { - return; - } - - if (_framebuffer == 0) - { - CreateRenderTarget(); - } - - int currentFramebuffer = GL.GetInteger(GetPName.FramebufferBinding); - - var image = _control.Image; - var fence = _control._fence; - - GL.BindFramebuffer(FramebufferTarget.Framebuffer, _framebuffer); - GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, image, 0); - GL.BindFramebuffer(FramebufferTarget.Framebuffer, currentFramebuffer); - - if (context is not ISkiaDrawingContextImpl skiaDrawingContextImpl) - { - return; - } - - var imageInfo = new SKImageInfo((int)_control.RenderSize.Width, (int)_control.RenderSize.Height, SKColorType.Rgba8888); - var glInfo = new GRGlFramebufferInfo((uint)_framebuffer, SKColorType.Rgba8888.ToGlSizedFormat()); - - GL.WaitSync(fence, WaitSyncFlags.None, ulong.MaxValue); - - using var backendTexture = new GRBackendRenderTarget(imageInfo.Width, imageInfo.Height, 1, 0, glInfo); - using var surface = SKSurface.Create(skiaDrawingContextImpl.GrContext, backendTexture, GRSurfaceOrigin.BottomLeft, SKColorType.Rgba8888); - - if (surface == null) - { - return; - } - - var rect = new Rect(new Point(), _control.RenderSize); - - using var snapshot = surface.Snapshot(); - skiaDrawingContextImpl.SkCanvas.DrawImage(snapshot, rect.ToSKRect(), _control.Bounds.ToSKRect(), new SKPaint()); - } - } + public abstract void DestroyBackgroundContext(); + internal abstract void MakeCurrent(); + internal abstract void MakeCurrent(SwappableNativeWindowBase window); } -} +}
\ No newline at end of file diff --git a/Ryujinx.Ava/Ui/Controls/VulkanRendererControl.cs b/Ryujinx.Ava/Ui/Controls/VulkanRendererControl.cs new file mode 100644 index 00000000..fdbd8df9 --- /dev/null +++ b/Ryujinx.Ava/Ui/Controls/VulkanRendererControl.cs @@ -0,0 +1,153 @@ +using Avalonia; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Skia; +using Avalonia.Threading; +using Ryujinx.Ava.Ui.Backend.Vulkan; +using Ryujinx.Ava.Ui.Vulkan; +using Ryujinx.Common.Configuration; +using Ryujinx.Graphics.Vulkan; +using Silk.NET.Vulkan; +using SkiaSharp; +using SPB.Windowing; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Ryujinx.Ava.Ui.Controls +{ + internal class VulkanRendererControl : RendererControl + { + private VulkanPlatformInterface _platformInterface; + + public VulkanRendererControl(GraphicsDebugLevel graphicsDebugLevel) : base(graphicsDebugLevel) + { + _platformInterface = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>(); + } + + public override void DestroyBackgroundContext() + { + + } + + protected override ICustomDrawOperation CreateDrawOperation() + { + return new VulkanDrawOperation(this); + } + + protected override void CreateWindow() + { + } + + internal override void MakeCurrent() + { + } + + internal override void MakeCurrent(SwappableNativeWindowBase window) + { + } + + internal override void Present(object image) + { + Dispatcher.UIThread.InvokeAsync(() => + { + Image = image; + }).Wait(); + + QueueRender(); + } + + private class VulkanDrawOperation : ICustomDrawOperation + { + public Rect Bounds { get; } + + private readonly VulkanRendererControl _control; + + public VulkanDrawOperation(VulkanRendererControl control) + { + _control = control; + Bounds = _control.Bounds; + } + + public void Dispose() + { + + } + + public bool Equals(ICustomDrawOperation other) + { + return other is VulkanDrawOperation operation && Equals(this, operation) && operation.Bounds == Bounds; + } + + public bool HitTest(Point p) + { + return Bounds.Contains(p); + } + + public void Render(IDrawingContextImpl context) + { + if (_control.Image == null || _control.RenderSize.Width == 0 || _control.RenderSize.Height == 0) + { + return; + } + + var image = (PresentImageInfo)_control.Image; + + if (context is not ISkiaDrawingContextImpl skiaDrawingContextImpl) + { + return; + } + + _control._platformInterface.Device.QueueWaitIdle(); + + var gpu = AvaloniaLocator.Current.GetService<VulkanSkiaGpu>(); + + var imageInfo = new GRVkImageInfo() + { + CurrentQueueFamily = _control._platformInterface.PhysicalDevice.QueueFamilyIndex, + Format = (uint)Format.R8G8B8A8Unorm, + Image = image.Image.Handle, + ImageLayout = (uint)ImageLayout.ColorAttachmentOptimal, + ImageTiling = (uint)ImageTiling.Optimal, + ImageUsageFlags = (uint)(ImageUsageFlags.ImageUsageColorAttachmentBit + | ImageUsageFlags.ImageUsageTransferSrcBit + | ImageUsageFlags.ImageUsageTransferDstBit), + LevelCount = 1, + SampleCount = 1, + Protected = false, + Alloc = new GRVkAlloc() + { + Memory = image.Memory.Handle, + Flags = 0, + Offset = image.MemoryOffset, + Size = image.MemorySize + } + }; + + using var backendTexture = new GRBackendRenderTarget( + (int)_control.RenderSize.Width, + (int)_control.RenderSize.Height, + 1, + imageInfo); + + using var surface = SKSurface.Create( + gpu.GrContext, + backendTexture, + GRSurfaceOrigin.TopLeft, + SKColorType.Rgba8888); + + if (surface == null) + { + return; + } + + var rect = new Rect(new Point(), _control.RenderSize); + + using var snapshot = surface.Snapshot(); + skiaDrawingContextImpl.SkCanvas.DrawImage(snapshot, rect.ToSKRect(), _control.Bounds.ToSKRect(), new SKPaint()); + } + } + } +} diff --git a/Ryujinx.Ava/Ui/Models/StatusUpdatedEventArgs.cs b/Ryujinx.Ava/Ui/Models/StatusUpdatedEventArgs.cs index 016c5cb0..154d0f6c 100644 --- a/Ryujinx.Ava/Ui/Models/StatusUpdatedEventArgs.cs +++ b/Ryujinx.Ava/Ui/Models/StatusUpdatedEventArgs.cs @@ -6,16 +6,18 @@ namespace Ryujinx.Ava.Ui.Models { public bool VSyncEnabled { get; } public float Volume { get; } + public string GpuBackend { get; } public string AspectRatio { get; } public string DockedMode { get; } public string FifoStatus { get; } public string GameStatus { get; } public string GpuName { get; } - public StatusUpdatedEventArgs(bool vSyncEnabled, float volume, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName) + public StatusUpdatedEventArgs(bool vSyncEnabled, float volume, string gpuBackend, string dockedMode, string aspectRatio, string gameStatus, string fifoStatus, string gpuName) { VSyncEnabled = vSyncEnabled; Volume = volume; + GpuBackend = gpuBackend; DockedMode = dockedMode; AspectRatio = aspectRatio; GameStatus = gameStatus; diff --git a/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs b/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs index bd9242e7..cd756d68 100644 --- a/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs +++ b/Ryujinx.Ava/Ui/ViewModels/MainWindowViewModel.cs @@ -197,6 +197,7 @@ namespace Ryujinx.Ava.Ui.ViewModels private string _pauseKey = "F5"; private string _screenshotkey = "F8"; private float _volume; + private string _backendText; public ApplicationData SelectedApplication { @@ -335,7 +336,7 @@ namespace Ryujinx.Ava.Ui.ViewModels } } - public string GpuStatusText + public string GpuNameText { get => _gpuStatusText; set @@ -346,6 +347,17 @@ namespace Ryujinx.Ava.Ui.ViewModels } } + public string BackendText + { + get => _backendText; + set + { + _backendText = value; + + OnPropertyChanged(); + } + } + public string DockedStatusText { get => _dockedStatusText; diff --git a/Ryujinx.Ava/Ui/ViewModels/SettingsViewModel.cs b/Ryujinx.Ava/Ui/ViewModels/SettingsViewModel.cs index 7b175f08..7b08923e 100644 --- a/Ryujinx.Ava/Ui/ViewModels/SettingsViewModel.cs +++ b/Ryujinx.Ava/Ui/ViewModels/SettingsViewModel.cs @@ -1,6 +1,8 @@ +using Avalonia; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Threading; +using DynamicData; using LibHac.Tools.FsSystem; using Ryujinx.Audio.Backends.OpenAL; using Ryujinx.Audio.Backends.SDL2; @@ -8,18 +10,26 @@ using Ryujinx.Audio.Backends.SoundIo; using Ryujinx.Ava.Common.Locale; using Ryujinx.Ava.Input; using Ryujinx.Ava.Ui.Controls; +using Ryujinx.Ava.Ui.Vulkan; using Ryujinx.Ava.Ui.Windows; using Ryujinx.Common.Configuration; using Ryujinx.Common.Configuration.Hid; using Ryujinx.Common.GraphicsDriver; using Ryujinx.Common.Logging; +using Ryujinx.Graphics.Vulkan; using Ryujinx.HLE.FileSystem; using Ryujinx.HLE.HOS.Services.Time.TimeZone; using Ryujinx.Input; using Ryujinx.Ui.Common.Configuration; using Ryujinx.Ui.Common.Configuration.System; +using Silk.NET.Vulkan; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; using TimeZone = Ryujinx.Ava.Ui.Models.TimeZone; namespace Ryujinx.Ava.Ui.ViewModels @@ -101,6 +111,7 @@ namespace Ryujinx.Ava.Ui.ViewModels public bool IgnoreMissingServices { get; set; } public bool ExpandDramSize { get; set; } public bool EnableShaderCache { get; set; } + public bool EnableTextureRecompression { get; set; } public bool EnableFileLog { get; set; } public bool EnableStub { get; set; } public bool EnableInfo { get; set; } @@ -115,6 +126,7 @@ namespace Ryujinx.Ava.Ui.ViewModels public bool IsSDL2Enabled { get; set; } public bool EnableCustomTheme { get; set; } public bool IsCustomResolutionScaleActive => _resolutionScale == 0; + public bool IsVulkanSelected => GraphicsBackendIndex == 0; public string TimeZone { get; set; } public string ShaderDumpPath { get; set; } @@ -129,6 +141,18 @@ namespace Ryujinx.Ava.Ui.ViewModels public int OpenglDebugLevel { get; set; } public int MemoryMode { get; set; } public int BaseStyleIndex { get; set; } + public int GraphicsBackendIndex + { + get => graphicsBackendIndex; + set + { + graphicsBackendIndex = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(IsVulkanSelected)); + } + } + + public int PreferredGpuIndex { get; set; } public float Volume { @@ -148,8 +172,11 @@ namespace Ryujinx.Ava.Ui.ViewModels public AvaloniaList<TimeZone> TimeZones { get; set; } public AvaloniaList<string> GameDirectories { get; set; } + public ObservableCollection<ComboBoxItem> AvailableGpus { get; set; } private KeyboardHotkeys _keyboardHotkeys; + private int graphicsBackendIndex; + private List<string> _gpuIds = new List<string>(); public KeyboardHotkeys KeyboardHotkeys { @@ -180,12 +207,14 @@ namespace Ryujinx.Ava.Ui.ViewModels { GameDirectories = new AvaloniaList<string>(); TimeZones = new AvaloniaList<TimeZone>(); + AvailableGpus = new ObservableCollection<ComboBoxItem>(); _validTzRegions = new List<string>(); CheckSoundBackends(); if (Program.PreviewerDetached) { + LoadAvailableGpus(); LoadCurrentConfiguration(); } } @@ -197,6 +226,34 @@ namespace Ryujinx.Ava.Ui.ViewModels IsSDL2Enabled = SDL2HardwareDeviceDriver.IsSupported; } + private unsafe void LoadAvailableGpus() + { + _gpuIds = new List<string>(); + List<string> names = new List<string>(); + if (!Program.UseVulkan) + { + var devices = VulkanRenderer.GetPhysicalDevices(); + foreach (var device in devices) + { + _gpuIds.Add(device.Id); + names.Add($"{device.Name} {(device.IsDiscrete ? "(dGpu)" : "")}"); + } + } + else + { + foreach (var device in VulkanPhysicalDevice.SuitableDevices) + { + _gpuIds.Add(VulkanInitialization.StringFromIdPair(device.Value.VendorID, device.Value.DeviceID)); + var value = device.Value; + var name = value.DeviceName; + names.Add($"{Marshal.PtrToStringAnsi((IntPtr)name)} {(device.Value.DeviceType == PhysicalDeviceType.DiscreteGpu ? "(dGpu)" : "")}"); + } + } + + AvailableGpus.Clear(); + AvailableGpus.AddRange(names.Select(x => new ComboBoxItem() { Content = x })); + } + public void LoadTimeZones() { _timeZoneContentManager = new TimeZoneContentManager(); @@ -266,6 +323,7 @@ namespace Ryujinx.Ava.Ui.ViewModels IgnoreMissingServices = config.System.IgnoreMissingServices; ExpandDramSize = config.System.ExpandRam; EnableShaderCache = config.Graphics.EnableShaderCache; + EnableTextureRecompression = config.Graphics.EnableTextureRecompression; EnableFileLog = config.Logger.EnableFileLog; EnableStub = config.Logger.EnableStub; EnableInfo = config.Logger.EnableInfo; @@ -286,6 +344,9 @@ namespace Ryujinx.Ava.Ui.ViewModels ShaderDumpPath = config.Graphics.ShadersDumpPath; CustomThemePath = config.Ui.CustomThemePath; BaseStyleIndex = config.Ui.BaseStyle == "Light" ? 0 : 1; + GraphicsBackendIndex = (int)config.Graphics.GraphicsBackend.Value; + + PreferredGpuIndex = _gpuIds.Contains(config.Graphics.PreferredGpu) ? _gpuIds.IndexOf(config.Graphics.PreferredGpu) : 0; Language = (int)config.System.Language.Value; Region = (int)config.System.Region.Value; @@ -313,7 +374,7 @@ namespace Ryujinx.Ava.Ui.ViewModels _previousVolumeLevel = Volume; } - public void SaveSettings() + public async Task SaveSettings() { List<string> gameDirs = new List<string>(GameDirectories); @@ -324,6 +385,8 @@ namespace Ryujinx.Ava.Ui.ViewModels config.System.TimeZone.Value = TimeZone; } + bool requiresRestart = config.Graphics.GraphicsBackend.Value != (GraphicsBackend)GraphicsBackendIndex; + config.Logger.EnableError.Value = EnableError; config.Logger.EnableTrace.Value = EnableTrace; config.Logger.EnableWarn.Value = EnableWarn; @@ -341,6 +404,8 @@ namespace Ryujinx.Ava.Ui.ViewModels config.HideCursorOnIdle.Value = HideCursorOnIdle; config.Graphics.EnableVsync.Value = EnableVsync; config.Graphics.EnableShaderCache.Value = EnableShaderCache; + config.Graphics.EnableTextureRecompression.Value = EnableTextureRecompression; + config.Graphics.GraphicsBackend.Value = (GraphicsBackend)GraphicsBackendIndex; config.System.EnablePtc.Value = EnablePptc; config.System.EnableInternetAccess.Value = EnableInternetAccess; config.System.EnableFsIntegrityChecks.Value = EnableFsIntegrityChecks; @@ -354,6 +419,20 @@ namespace Ryujinx.Ava.Ui.ViewModels config.System.Language.Value = (Language)Language; config.System.Region.Value = (Region)Region; + var selectedGpu = _gpuIds.ElementAtOrDefault(PreferredGpuIndex); + if (!requiresRestart) + { + var platform = AvaloniaLocator.Current.GetService<VulkanPlatformInterface>(); + if (platform != null) + { + var physicalDevice = platform.PhysicalDevice; + + requiresRestart = physicalDevice.DeviceId != selectedGpu; + } + } + + config.Graphics.PreferredGpu.Value = selectedGpu; + if (ConfigurationState.Instance.Graphics.BackendThreading != (BackendThreading)GraphicsBackendMultithreadingIndex) { DriverUtilities.ToggleOGLThreading(GraphicsBackendMultithreadingIndex == (int)BackendThreading.Off); @@ -392,6 +471,20 @@ namespace Ryujinx.Ava.Ui.ViewModels MainWindow.UpdateGraphicsConfig(); _previousVolumeLevel = Volume; + + if (requiresRestart) + { + var choice = await ContentDialogHelper.CreateChoiceDialog( + LocaleManager.Instance["SettingsAppRequiredRestartMessage"], + LocaleManager.Instance["SettingsGpuBackendRestartMessage"], + LocaleManager.Instance["SettingsGpuBackendRestartSubMessage"]); + + if (choice) + { + Process.Start(Environment.ProcessPath); + Environment.Exit(0); + } + } } public void RevertIfNotSaved() diff --git a/Ryujinx.Ava/Ui/Windows/MainWindow.axaml b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml index a08ca910..b248a484 100644 --- a/Ryujinx.Ava/Ui/Windows/MainWindow.axaml +++ b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml @@ -721,7 +721,21 @@ HorizontalAlignment="Left" VerticalAlignment="Center" IsVisible="{Binding !ShowLoadProgress}" - Text="{Binding GpuStatusText}" + Text="{Binding BackendText}" + TextAlignment="Left" /> + <Border + Width="2" + Height="12" + Margin="2,0" + BorderBrush="Gray" + BorderThickness="1" + IsVisible="{Binding !ShowLoadProgress}" /> + <TextBlock + Margin="5,0,5,0" + HorizontalAlignment="Left" + VerticalAlignment="Center" + IsVisible="{Binding !ShowLoadProgress}" + Text="{Binding GpuNameText}" TextAlignment="Left" /> </StackPanel> <StackPanel diff --git a/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs index a5e3c06a..73e5e099 100644 --- a/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs +++ b/Ryujinx.Ava/Ui/Windows/MainWindow.axaml.cs @@ -12,6 +12,7 @@ using Ryujinx.Ava.Ui.Applet; using Ryujinx.Ava.Ui.Controls; using Ryujinx.Ava.Ui.Models; using Ryujinx.Ava.Ui.ViewModels; +using Ryujinx.Ava.Ui.Vulkan; using Ryujinx.Common.Configuration; using Ryujinx.Common.Logging; using Ryujinx.Graphics.Gpu; @@ -59,7 +60,7 @@ namespace Ryujinx.Ava.Ui.Windows internal AppHost AppHost { get; private set; } public InputManager InputManager { get; private set; } - internal RendererControl GlRenderer { get; private set; } + internal RendererControl RendererControl { get; private set; } internal MainWindowViewModel ViewModel { get; private set; } public SettingsWindow SettingsWindow { get; set; } @@ -140,7 +141,8 @@ namespace Ryujinx.Ava.Ui.Windows ViewModel.AspectRatioStatusText = args.AspectRatio; ViewModel.GameStatusText = args.GameStatus; ViewModel.FifoStatusText = args.FifoStatus; - ViewModel.GpuStatusText = args.GpuName; + ViewModel.GpuNameText = args.GpuName; + ViewModel.BackendText = args.GpuBackend; ViewModel.ShowStatusSeparator = true; }); @@ -237,8 +239,8 @@ namespace Ryujinx.Ava.Ui.Windows _mainViewContent = MainContent.Content as Control; - GlRenderer = new RendererControl(3, 3, ConfigurationState.Instance.Logger.GraphicsDebugLevel); - AppHost = new AppHost(GlRenderer, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this); + RendererControl = Program.UseVulkan ? new VulkanRendererControl(ConfigurationState.Instance.Logger.GraphicsDebugLevel) : new OpenGLRendererControl(3, 3, ConfigurationState.Instance.Logger.GraphicsDebugLevel); + AppHost = new AppHost(RendererControl, InputManager, path, VirtualFileSystem, ContentManager, AccountManager, _userChannelPersistence, this); if (!AppHost.LoadGuestApplication().Result) { @@ -262,7 +264,7 @@ namespace Ryujinx.Ava.Ui.Windows private void InitializeGame() { - GlRenderer.GlInitialized += GlRenderer_Created; + RendererControl.RendererInitialized += GlRenderer_Created; AppHost.StatusUpdatedEvent += Update_StatusBar; AppHost.AppExit += AppHost_AppExit; @@ -302,14 +304,14 @@ namespace Ryujinx.Ava.Ui.Windows Dispatcher.UIThread.InvokeAsync(() => { - MainContent.Content = GlRenderer; + MainContent.Content = RendererControl; if (startFullscreen && WindowState != WindowState.FullScreen) { ViewModel.ToggleFullscreen(); } - GlRenderer.Focus(); + RendererControl.Focus(); }); } @@ -361,8 +363,9 @@ namespace Ryujinx.Ava.Ui.Windows HandleRelaunch(); }); - GlRenderer.GlInitialized -= GlRenderer_Created; - GlRenderer = null; + + RendererControl.RendererInitialized -= GlRenderer_Created; + RendererControl = null; ViewModel.SelectedIcon = null; @@ -513,6 +516,7 @@ namespace Ryujinx.Ava.Ui.Windows GraphicsConfig.MaxAnisotropy = ConfigurationState.Instance.Graphics.MaxAnisotropy; GraphicsConfig.ShadersDumpPath = ConfigurationState.Instance.Graphics.ShadersDumpPath; GraphicsConfig.EnableShaderCache = ConfigurationState.Instance.Graphics.EnableShaderCache; + GraphicsConfig.EnableTextureRecompression = ConfigurationState.Instance.Graphics.EnableTextureRecompression; } public void LoadHotKeys() diff --git a/Ryujinx.Ava/Ui/Windows/SettingsWindow.axaml b/Ryujinx.Ava/Ui/Windows/SettingsWindow.axaml index 9a9a395e..0e5978b7 100644 --- a/Ryujinx.Ava/Ui/Windows/SettingsWindow.axaml +++ b/Ryujinx.Ava/Ui/Windows/SettingsWindow.axaml @@ -504,43 +504,51 @@ VerticalScrollBarVisibility="Auto"> <Border> <StackPanel - Margin="10, 5" + Margin="10,5" HorizontalAlignment="Stretch" Orientation="Vertical" Spacing="10"> - <TextBlock FontWeight="Bold" Text="{locale:Locale SettingsTabGraphicsEnhancements}" /> + <TextBlock FontWeight="Bold" Text="{locale:Locale SettingsTabGraphicsAPI}" /> <StackPanel Margin="10,0,0,0" Orientation="Vertical" Spacing="10"> - <CheckBox IsChecked="{Binding EnableShaderCache}" - ToolTip.Tip="{locale:Locale ShaderCacheToggleTooltip}"> - <TextBlock Text="{locale:Locale SettingsTabGraphicsEnableShaderCache}" /> - </CheckBox> <StackPanel Orientation="Horizontal"> - <TextBlock VerticalAlignment="Center" - ToolTip.Tip="{locale:Locale AnisotropyTooltip}" - Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering}" - Width="250" /> - <ComboBox SelectedIndex="{Binding MaxAnisotropy}" - Width="350" - HorizontalContentAlignment="Left" - ToolTip.Tip="{locale:Locale AnisotropyTooltip}"> - <ComboBoxItem> - <TextBlock - Text="{locale:Locale SettingsTabGraphicsAnisotropicFilteringAuto}" /> - </ComboBoxItem> - <ComboBoxItem> - <TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering2x}" /> - </ComboBoxItem> - <ComboBoxItem> - <TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering4x}" /> - </ComboBoxItem> - <ComboBoxItem> - <TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering8x}" /> - </ComboBoxItem> - <ComboBoxItem> - <TextBlock - Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering16x}" /> - </ComboBoxItem> - </ComboBox> + <TextBlock VerticalAlignment="Center" + ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}" + Text="{locale:Locale SettingsTabGraphicsBackend}" + Width="250" /> + <ComboBox Width="350" + HorizontalContentAlignment="Left" + ToolTip.Tip="{locale:Locale SettingsTabGraphicsBackendTooltip}" + SelectedIndex="{Binding GraphicsBackendIndex}"> + <ComboBoxItem> + <TextBlock Text="Vulkan" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="OpenGL" /> + </ComboBoxItem> + </ComboBox> + </StackPanel> + <StackPanel Orientation="Horizontal" IsVisible="{Binding IsVulkanSelected}"> + <TextBlock VerticalAlignment="Center" + ToolTip.Tip="{locale:Locale SettingsTabGraphicsPreferredGpuTooltip}" + Text="{locale:Locale SettingsTabGraphicsPreferredGpu}" + Width="250" /> + <ComboBox Width="350" + HorizontalContentAlignment="Left" + ToolTip.Tip="{locale:Locale SettingsTabGraphicsPreferredGpuTooltip}" + SelectedIndex="{Binding PreferredGpuIndex}" + Items="{Binding AvailableGpus}"/> + </StackPanel> + <Separator Height="1" /> + <TextBlock FontWeight="Bold" Text="{locale:Locale SettingsTabGraphicsFeatures}" /> + <StackPanel Orientation="Vertical"> + <CheckBox IsChecked="{Binding EnableShaderCache}" + ToolTip.Tip="{locale:Locale ShaderCacheToggleTooltip}"> + <TextBlock Text="{locale:Locale SettingsTabGraphicsEnableShaderCache}" /> + </CheckBox> + <CheckBox IsChecked="{Binding EnableTextureRecompression}" + ToolTip.Tip="{locale:Locale SettingsEnableTextureRecompressionTooltip}"> + <TextBlock Text="{locale:Locale SettingsEnableTextureRecompression}" /> + </CheckBox> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock VerticalAlignment="Center" @@ -582,6 +590,34 @@ </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock VerticalAlignment="Center" + ToolTip.Tip="{locale:Locale AnisotropyTooltip}" + Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering}" + Width="250" /> + <ComboBox SelectedIndex="{Binding MaxAnisotropy}" + Width="350" + HorizontalContentAlignment="Left" + ToolTip.Tip="{locale:Locale AnisotropyTooltip}"> + <ComboBoxItem> + <TextBlock + Text="{locale:Locale SettingsTabGraphicsAnisotropicFilteringAuto}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering2x}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering4x}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering8x}" /> + </ComboBoxItem> + <ComboBoxItem> + <TextBlock + Text="{locale:Locale SettingsTabGraphicsAnisotropicFiltering16x}" /> + </ComboBoxItem> + </ComboBox> + </StackPanel> + <StackPanel Orientation="Horizontal"> + <TextBlock VerticalAlignment="Center" ToolTip.Tip="{locale:Locale AspectRatioTooltip}" Text="{locale:Locale SettingsTabGraphicsAspectRatio}" Width="250" /> @@ -610,8 +646,6 @@ </ComboBox> </StackPanel> </StackPanel> - <Separator Height="1" /> - <TextBlock FontWeight="Bold" Text="{locale:Locale SettingsTabGraphicsFeatures}" /> <StackPanel Margin="10,0,0,0" HorizontalAlignment="Stretch" diff --git a/Ryujinx.Ava/Ui/Windows/SettingsWindow.axaml.cs b/Ryujinx.Ava/Ui/Windows/SettingsWindow.axaml.cs index cdac6ef6..97e9bfed 100644 --- a/Ryujinx.Ava/Ui/Windows/SettingsWindow.axaml.cs +++ b/Ryujinx.Ava/Ui/Windows/SettingsWindow.axaml.cs @@ -1,4 +1,3 @@ -using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Data; @@ -15,9 +14,9 @@ using Ryujinx.Input; using Ryujinx.Input.Assigner; using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; +using System.Threading.Tasks; using TimeZone = Ryujinx.Ava.Ui.Models.TimeZone; namespace Ryujinx.Ava.Ui.Windows @@ -97,7 +96,7 @@ namespace Ryujinx.Ava.Ui.Windows } } } - } + } private void Button_Unchecked(object sender, RoutedEventArgs e) { @@ -209,9 +208,9 @@ namespace Ryujinx.Ava.Ui.Windows } } - private void SaveButton_Clicked(object sender, RoutedEventArgs e) + private async void SaveButton_Clicked(object sender, RoutedEventArgs e) { - SaveSettings(); + await SaveSettings(); Close(); } @@ -222,14 +221,14 @@ namespace Ryujinx.Ava.Ui.Windows Close(); } - private void ApplyButton_Clicked(object sender, RoutedEventArgs e) + private async void ApplyButton_Clicked(object sender, RoutedEventArgs e) { - SaveSettings(); + await SaveSettings(); } - private void SaveSettings() + private async Task SaveSettings() { - ViewModel.SaveSettings(); + await ViewModel.SaveSettings(); ControllerSettings?.SaveCurrentProfile(); |