Skip to content

Commit 7b7f9de

Browse files
Add GPU-less Avalonia rendering fallback
Detect Windows hosts without a hardware GPU and configure Avalonia to use software rendering with reduced motion. Add a settings toggle to opt out of the automatic fallback.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent f801a20 commit 7b7f9de

7 files changed

Lines changed: 232 additions & 3 deletions

File tree

src/Languages/lang_en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,8 @@
471471
"System language": "System language",
472472
"Is your language missing or incomplete?": "Is your language missing or incomplete?",
473473
"Appearance": "Appearance",
474+
"Rendering": "Rendering",
475+
"Automatically switch to software rendering when Windows has no hardware GPU": "Automatically switch to software rendering when Windows has no hardware GPU",
474476
"UniGetUI on the background and system tray": "UniGetUI on the background and system tray",
475477
"Package lists": "Package lists",
476478
"Use classic mode": "Use classic mode",

src/UniGetUI.Avalonia/Infrastructure/AvaloniaAppHost.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using System.Runtime.InteropServices;
22
using Avalonia;
3+
#if WINDOWS
4+
using Avalonia.Win32;
5+
#endif
36
using Avalonia.Threading;
47
using UniGetUI.Core.Data;
58
using UniGetUI.Core.Logging;
@@ -89,9 +92,22 @@ Welcome to UniGetUI Version {CoreData.VersionName}
8992
}
9093

9194
public static AppBuilder BuildAvaloniaApp()
92-
=> AppBuilder.Configure<App>()
93-
.UsePlatformDetect()
94-
.LogToTrace();
95+
{
96+
AppBuilder builder = AppBuilder.Configure<App>()
97+
.UsePlatformDetect();
98+
99+
#if WINDOWS
100+
if (WindowsAvaloniaRenderingPolicy.ShouldUseSoftwareRendering)
101+
{
102+
builder = builder.With(new Win32PlatformOptions
103+
{
104+
RenderingMode = [Win32RenderingMode.Software],
105+
});
106+
}
107+
#endif
108+
109+
return builder.LogToTrace();
110+
}
95111

96112
private static bool ShouldPrepareCliConsole(IReadOnlyList<string> args)
97113
{

src/UniGetUI.Avalonia/Infrastructure/MotionPreference.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@ public static bool ReducedMotion
2323
get
2424
{
2525
if (OperatingSystem.IsWindows())
26+
{
27+
if (WindowsAvaloniaRenderingPolicy.ShouldReduceMotion)
28+
return true;
29+
2630
return GetWindowsReducedMotion();
31+
}
32+
2733
return _cachedUnix ??= GetUnixReducedMotion();
2834
}
2935
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
using System.Diagnostics;
2+
using System.Runtime.InteropServices;
3+
using Avalonia.Controls;
4+
using UniGetUI.Core.Logging;
5+
using CoreSettings = global::UniGetUI.Core.SettingsEngine.Settings;
6+
7+
namespace UniGetUI.Avalonia.Infrastructure;
8+
9+
internal static class WindowsAvaloniaRenderingPolicy
10+
{
11+
private static bool? _hasHardwareGpu;
12+
private static bool? _shouldUseSoftwareRendering;
13+
14+
public static bool ShouldUseSoftwareRendering
15+
{
16+
get
17+
{
18+
if (_shouldUseSoftwareRendering is not null)
19+
return _shouldUseSoftwareRendering.Value;
20+
21+
if (!OperatingSystem.IsWindows() || Design.IsDesignMode)
22+
return false;
23+
24+
if (CoreSettings.Get(CoreSettings.K.DisableAutoSoftwareRenderingOnGpuLessHosts))
25+
return false;
26+
27+
_shouldUseSoftwareRendering = !HasHardwareGpu;
28+
if (_shouldUseSoftwareRendering.Value)
29+
{
30+
Logger.Warn(
31+
"No hardware GPU detected. Using Avalonia software rendering and reduced motion.");
32+
}
33+
34+
return _shouldUseSoftwareRendering.Value;
35+
}
36+
}
37+
38+
public static bool ShouldReduceMotion => ShouldUseSoftwareRendering;
39+
40+
private static bool HasHardwareGpu
41+
{
42+
get
43+
{
44+
if (_hasHardwareGpu is not null)
45+
return _hasHardwareGpu.Value;
46+
47+
Stopwatch stopwatch = Stopwatch.StartNew();
48+
_hasHardwareGpu = DetectHardwareGpu();
49+
stopwatch.Stop();
50+
51+
Logger.Info(
52+
$"DXGI hardware GPU detection took {stopwatch.Elapsed.TotalMilliseconds:F1} ms; hardware GPU: {_hasHardwareGpu.Value}");
53+
54+
return _hasHardwareGpu.Value;
55+
}
56+
}
57+
58+
private static bool DetectHardwareGpu()
59+
{
60+
try
61+
{
62+
Guid factoryIid = typeof(IDXGIFactory1).GUID;
63+
if (CreateDXGIFactory1(ref factoryIid, out object factoryObj) != HResult.Ok
64+
|| factoryObj is not IDXGIFactory1 factory)
65+
{
66+
Logger.Warn("Could not create DXGI factory; assuming a hardware GPU is present.");
67+
return true;
68+
}
69+
70+
try
71+
{
72+
for (uint i = 0; ; i++)
73+
{
74+
int hr = factory.EnumAdapters1(i, out IDXGIAdapter1 adapter);
75+
if (hr == HResult.DxgiErrorNotFound || hr != HResult.Ok || adapter is null)
76+
break;
77+
78+
try
79+
{
80+
if (adapter.GetDesc1(out DXGI_ADAPTER_DESC1 desc) != HResult.Ok)
81+
continue;
82+
83+
bool isSoftwareAdapter =
84+
(desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE) != 0
85+
|| (desc.VendorId == MicrosoftVendorId
86+
&& desc.DeviceId == BasicRenderDriverDeviceId);
87+
88+
if (!isSoftwareAdapter)
89+
return true;
90+
}
91+
finally
92+
{
93+
Marshal.ReleaseComObject(adapter);
94+
}
95+
}
96+
}
97+
finally
98+
{
99+
Marshal.ReleaseComObject(factory);
100+
}
101+
102+
return false;
103+
}
104+
catch (Exception ex)
105+
{
106+
Logger.Warn("Could not detect DXGI hardware GPU; assuming one is present.");
107+
Logger.Warn(ex);
108+
return true;
109+
}
110+
}
111+
112+
[DllImport("dxgi.dll", ExactSpelling = true)]
113+
private static extern int CreateDXGIFactory1(
114+
ref Guid riid,
115+
[MarshalAs(UnmanagedType.IUnknown)] out object ppFactory
116+
);
117+
118+
private static class HResult
119+
{
120+
public const int Ok = 0;
121+
public const int DxgiErrorNotFound = unchecked((int)0x887A0002);
122+
}
123+
124+
private const uint DXGI_ADAPTER_FLAG_SOFTWARE = 2;
125+
126+
// Microsoft Basic Render Driver (WARP) is enumerated with this VendorId/DeviceId pair.
127+
private const uint MicrosoftVendorId = 0x1414;
128+
private const uint BasicRenderDriverDeviceId = 0x8C;
129+
130+
[ComImport]
131+
[Guid("770aae78-f26f-4dba-a829-253c83d1b387")]
132+
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
133+
private interface IDXGIFactory1
134+
{
135+
void SetPrivateData();
136+
void SetPrivateDataInterface();
137+
void GetPrivateData();
138+
void GetParent();
139+
void EnumAdapters();
140+
void MakeWindowAssociation();
141+
void GetWindowAssociation();
142+
void CreateSwapChain();
143+
void CreateSoftwareAdapter();
144+
145+
[PreserveSig]
146+
int EnumAdapters1(uint adapter, out IDXGIAdapter1 ppAdapter);
147+
148+
[PreserveSig]
149+
bool IsCurrent();
150+
}
151+
152+
[ComImport]
153+
[Guid("29038f61-3839-4626-91fd-086879011a05")]
154+
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
155+
private interface IDXGIAdapter1
156+
{
157+
void SetPrivateData();
158+
void SetPrivateDataInterface();
159+
void GetPrivateData();
160+
void GetParent();
161+
void EnumOutputs();
162+
void GetDesc();
163+
void CheckInterfaceSupport();
164+
165+
[PreserveSig]
166+
int GetDesc1(out DXGI_ADAPTER_DESC1 pDesc);
167+
}
168+
169+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
170+
private struct DXGI_ADAPTER_DESC1
171+
{
172+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
173+
public string Description;
174+
175+
public uint VendorId;
176+
public uint DeviceId;
177+
public uint SubSysId;
178+
public uint Revision;
179+
public UIntPtr DedicatedVideoMemory;
180+
public UIntPtr DedicatedSystemMemory;
181+
public UIntPtr SharedSystemMemory;
182+
public uint AdapterLuidLowPart;
183+
public int AdapterLuidHighPart;
184+
public uint Flags;
185+
}
186+
}

src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@
239239
<Compile Include="Infrastructure\RelayCommand.cs" />
240240
<Compile Include="Infrastructure\SingleInstanceRedirector.cs" />
241241
<Compile Include="Infrastructure\UninstallConfirmationDialog.cs" />
242+
<Compile Include="Infrastructure\WindowsAvaloniaRenderingPolicy.cs" />
242243
<Compile Update="App.axaml.cs">
243244
<DependentUpon>App.axaml</DependentUpon>
244245
</Compile>

src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Interface_P.axaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,22 @@
2626
CornerRadius="0,0,8,8"
2727
BorderThickness="1,0,1,1"/>
2828

29+
<Border Height="16"/>
30+
31+
<settings:TranslatedTextBlock Text="Rendering"
32+
FontSize="14"
33+
FontWeight="SemiBold"
34+
Margin="44,32,4,8"
35+
automation:AutomationProperties.HeadingLevel="2"
36+
IsVisible="{Binding IsWindows}"/>
37+
38+
<settings:CheckboxCard SettingName="DisableAutoSoftwareRenderingOnGpuLessHosts"
39+
Text="{t:Translate Automatically switch to software rendering when Windows has no hardware GPU}"
40+
WarningText="{t:Translate Restart UniGetUI to apply this change}"
41+
StateChangedCommand="{Binding ShowRestartRequiredCommand}"
42+
CornerRadius="8"
43+
IsVisible="{Binding IsWindows}"/>
44+
2945
<Border Height="16"/>
3046

3147
<StackPanel x:Name="SystemTraySection" Orientation="Vertical">

src/UniGetUI.Core.Settings/SettingsEngine_Names.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ public enum K
9797
RedactUsernameInLog,
9898
DisableReleaseNotesOnUpdate,
9999
LastKnownBuildNumber,
100+
DisableAutoSoftwareRenderingOnGpuLessHosts,
100101

101102
Test1,
102103
Test2,
@@ -205,6 +206,7 @@ public static string ResolveKey(K key)
205206
K.RedactUsernameInLog => "RedactUsernameInLog",
206207
K.DisableReleaseNotesOnUpdate => "DisableReleaseNotesOnUpdate",
207208
K.LastKnownBuildNumber => "LastKnownBuildNumber",
209+
K.DisableAutoSoftwareRenderingOnGpuLessHosts => "DisableAutoSoftwareRenderingOnGpuLessHosts",
208210

209211
K.Test1 => "TestSetting1",
210212
K.Test2 => "TestSetting2",

0 commit comments

Comments
 (0)