diff --git a/src/Languages/lang_en.json b/src/Languages/lang_en.json index 295507dc9..5c29d62a5 100644 --- a/src/Languages/lang_en.json +++ b/src/Languages/lang_en.json @@ -471,6 +471,8 @@ "System language": "System language", "Is your language missing or incomplete?": "Is your language missing or incomplete?", "Appearance": "Appearance", + "Rendering": "Rendering", + "Automatically switch to software rendering when Windows has no hardware GPU": "Automatically switch to software rendering when Windows has no hardware GPU", "UniGetUI on the background and system tray": "UniGetUI on the background and system tray", "Package lists": "Package lists", "Use classic mode": "Use classic mode", diff --git a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaAppHost.cs b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaAppHost.cs index dcc31310a..fb417b108 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaAppHost.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaAppHost.cs @@ -1,5 +1,8 @@ using System.Runtime.InteropServices; using Avalonia; +#if WINDOWS +using Avalonia.Win32; +#endif using Avalonia.Threading; using UniGetUI.Core.Data; using UniGetUI.Core.Logging; @@ -89,9 +92,22 @@ Welcome to UniGetUI Version {CoreData.VersionName} } public static AppBuilder BuildAvaloniaApp() - => AppBuilder.Configure() - .UsePlatformDetect() - .LogToTrace(); + { + AppBuilder builder = AppBuilder.Configure() + .UsePlatformDetect(); + +#if WINDOWS + if (WindowsAvaloniaRenderingPolicy.ShouldUseSoftwareRendering) + { + builder = builder.With(new Win32PlatformOptions + { + RenderingMode = [Win32RenderingMode.Software], + }); + } +#endif + + return builder.LogToTrace(); + } private static bool ShouldPrepareCliConsole(IReadOnlyList args) { diff --git a/src/UniGetUI.Avalonia/Infrastructure/MotionPreference.cs b/src/UniGetUI.Avalonia/Infrastructure/MotionPreference.cs index 766395477..47ea897f1 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/MotionPreference.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/MotionPreference.cs @@ -23,7 +23,13 @@ public static bool ReducedMotion get { if (OperatingSystem.IsWindows()) + { + if (WindowsAvaloniaRenderingPolicy.ShouldReduceMotion) + return true; + return GetWindowsReducedMotion(); + } + return _cachedUnix ??= GetUnixReducedMotion(); } } diff --git a/src/UniGetUI.Avalonia/Infrastructure/WindowsAvaloniaRenderingPolicy.cs b/src/UniGetUI.Avalonia/Infrastructure/WindowsAvaloniaRenderingPolicy.cs new file mode 100644 index 000000000..45867f210 --- /dev/null +++ b/src/UniGetUI.Avalonia/Infrastructure/WindowsAvaloniaRenderingPolicy.cs @@ -0,0 +1,186 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using Avalonia.Controls; +using UniGetUI.Core.Logging; +using CoreSettings = global::UniGetUI.Core.SettingsEngine.Settings; + +namespace UniGetUI.Avalonia.Infrastructure; + +internal static class WindowsAvaloniaRenderingPolicy +{ + private static bool? _hasHardwareGpu; + private static bool? _shouldUseSoftwareRendering; + + public static bool ShouldUseSoftwareRendering + { + get + { + if (_shouldUseSoftwareRendering is not null) + return _shouldUseSoftwareRendering.Value; + + if (!OperatingSystem.IsWindows() || Design.IsDesignMode) + return false; + + if (CoreSettings.Get(CoreSettings.K.DisableAutoSoftwareRenderingOnGpuLessHosts)) + return false; + + _shouldUseSoftwareRendering = !HasHardwareGpu; + if (_shouldUseSoftwareRendering.Value) + { + Logger.Warn( + "No hardware GPU detected. Using Avalonia software rendering and reduced motion."); + } + + return _shouldUseSoftwareRendering.Value; + } + } + + public static bool ShouldReduceMotion => ShouldUseSoftwareRendering; + + private static bool HasHardwareGpu + { + get + { + if (_hasHardwareGpu is not null) + return _hasHardwareGpu.Value; + + Stopwatch stopwatch = Stopwatch.StartNew(); + _hasHardwareGpu = DetectHardwareGpu(); + stopwatch.Stop(); + + Logger.Info( + $"DXGI hardware GPU detection took {stopwatch.Elapsed.TotalMilliseconds:F1} ms; hardware GPU: {_hasHardwareGpu.Value}"); + + return _hasHardwareGpu.Value; + } + } + + private static bool DetectHardwareGpu() + { + try + { + Guid factoryIid = typeof(IDXGIFactory1).GUID; + if (CreateDXGIFactory1(ref factoryIid, out object factoryObj) != HResult.Ok + || factoryObj is not IDXGIFactory1 factory) + { + Logger.Warn("Could not create DXGI factory; assuming a hardware GPU is present."); + return true; + } + + try + { + for (uint i = 0; ; i++) + { + int hr = factory.EnumAdapters1(i, out IDXGIAdapter1 adapter); + if (hr == HResult.DxgiErrorNotFound || hr != HResult.Ok || adapter is null) + break; + + try + { + if (adapter.GetDesc1(out DXGI_ADAPTER_DESC1 desc) != HResult.Ok) + continue; + + bool isSoftwareAdapter = + (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) != 0 + || (desc.VendorId == MicrosoftVendorId + && desc.DeviceId == BasicRenderDriverDeviceId); + + if (!isSoftwareAdapter) + return true; + } + finally + { + Marshal.ReleaseComObject(adapter); + } + } + } + finally + { + Marshal.ReleaseComObject(factory); + } + + return false; + } + catch (Exception ex) + { + Logger.Warn("Could not detect DXGI hardware GPU; assuming one is present."); + Logger.Warn(ex); + return true; + } + } + + [DllImport("dxgi.dll", ExactSpelling = true)] + private static extern int CreateDXGIFactory1( + ref Guid riid, + [MarshalAs(UnmanagedType.IUnknown)] out object ppFactory + ); + + private static class HResult + { + public const int Ok = 0; + public const int DxgiErrorNotFound = unchecked((int)0x887A0002); + } + + private const uint DXGI_ADAPTER_FLAG_SOFTWARE = 2; + + // Microsoft Basic Render Driver (WARP) is enumerated with this VendorId/DeviceId pair. + private const uint MicrosoftVendorId = 0x1414; + private const uint BasicRenderDriverDeviceId = 0x8C; + + [ComImport] + [Guid("770aae78-f26f-4dba-a829-253c83d1b387")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IDXGIFactory1 + { + void SetPrivateData(); + void SetPrivateDataInterface(); + void GetPrivateData(); + void GetParent(); + void EnumAdapters(); + void MakeWindowAssociation(); + void GetWindowAssociation(); + void CreateSwapChain(); + void CreateSoftwareAdapter(); + + [PreserveSig] + int EnumAdapters1(uint adapter, out IDXGIAdapter1 ppAdapter); + + [PreserveSig] + bool IsCurrent(); + } + + [ComImport] + [Guid("29038f61-3839-4626-91fd-086879011a05")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IDXGIAdapter1 + { + void SetPrivateData(); + void SetPrivateDataInterface(); + void GetPrivateData(); + void GetParent(); + void EnumOutputs(); + void GetDesc(); + void CheckInterfaceSupport(); + + [PreserveSig] + int GetDesc1(out DXGI_ADAPTER_DESC1 pDesc); + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct DXGI_ADAPTER_DESC1 + { + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] + public string Description; + + public uint VendorId; + public uint DeviceId; + public uint SubSysId; + public uint Revision; + public UIntPtr DedicatedVideoMemory; + public UIntPtr DedicatedSystemMemory; + public UIntPtr SharedSystemMemory; + public uint AdapterLuidLowPart; + public int AdapterLuidHighPart; + public uint Flags; + } +} diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj index f0c4a29fe..cec80b9ed 100644 --- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj +++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj @@ -239,6 +239,7 @@ + App.axaml diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml index a4dfcc918..3b56a5789 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml @@ -26,6 +26,22 @@ CornerRadius="0,0,8,8" BorderThickness="1,0,1,1"/> + + + + + + diff --git a/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs b/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs index 630668eda..5a55f4341 100644 --- a/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs +++ b/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs @@ -97,6 +97,7 @@ public enum K RedactUsernameInLog, DisableReleaseNotesOnUpdate, LastKnownBuildNumber, + DisableAutoSoftwareRenderingOnGpuLessHosts, Test1, Test2, @@ -205,6 +206,7 @@ public static string ResolveKey(K key) K.RedactUsernameInLog => "RedactUsernameInLog", K.DisableReleaseNotesOnUpdate => "DisableReleaseNotesOnUpdate", K.LastKnownBuildNumber => "LastKnownBuildNumber", + K.DisableAutoSoftwareRenderingOnGpuLessHosts => "DisableAutoSoftwareRenderingOnGpuLessHosts", K.Test1 => "TestSetting1", K.Test2 => "TestSetting2",