diff --git a/UniGetUI.iss b/UniGetUI.iss
index e0a5ebabd7..03a68365aa 100644
--- a/UniGetUI.iss
+++ b/UniGetUI.iss
@@ -318,7 +318,7 @@ Source: "InstallerExtras\ForceUniGetUIPortable"; DestDir: "{app}"; Tasks: portab
[Icons]
-Name: "{autostartmenu}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: regularinstall\startmenuicon
+Name: "{autostartmenu}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; AppUserModelID: "Devolutions.UniGetUI"; Tasks: regularinstall\startmenuicon
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: regularinstall\desktopicon
[Run]
diff --git a/src/UniGetUI.Avalonia/Infrastructure/AppShortcutAumidStamper.cs b/src/UniGetUI.Avalonia/Infrastructure/AppShortcutAumidStamper.cs
deleted file mode 100644
index 2c9ef2f5fb..0000000000
--- a/src/UniGetUI.Avalonia/Infrastructure/AppShortcutAumidStamper.cs
+++ /dev/null
@@ -1,226 +0,0 @@
-#if WINDOWS
-using System;
-using System.Diagnostics.CodeAnalysis;
-using System.IO;
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using UniGetUI.Core.Logging;
-
-namespace UniGetUI.Avalonia.Infrastructure;
-
-///
-/// Stamps the UniGetUI AppUserModelID onto the Start Menu shortcut the installer
-/// creates, which is the shell's "app is installed" signal required before Windows will
-/// surface Action-Center toasts for an unpackaged Win32 app.
-///
-///
-/// Inno Setup cannot write shortcut property-store values directly, so the app performs
-/// this idempotent fix-up on first launch (and on every launch; it is cheap). The AUMID
-/// must match the one set on the process by .
-///
-internal static class AppShortcutAumidStamper
-{
- private const string ShortcutName = "UniGetUI";
- private const string Aumid = Win32ToastNotifier.AppUserModelId;
-
- // System.AppUserModel.Id property key: {9F4C2855-9F79-4B39-A8D0-E1A8F39EFCDC}, pid 5
- private static readonly PropertyKey AppUserModelIdKey =
- new(new Guid("9F4C2855-9F79-4B39-A8D0-E1A8F39EFCDC"), 5);
-
- public static void EnsureStamped()
- {
- if (!OperatingSystem.IsWindows())
- return;
-
- if (!RuntimeFeature.IsDynamicCodeSupported)
- {
- Logger.Info("Skipping Start Menu shortcut AUMID stamping because NativeAOT does not support legacy COM activation.");
- return;
- }
-
- try
- {
- string? shortcutPath = ResolveStartMenuShortcut();
- if (shortcutPath is null || !File.Exists(shortcutPath))
- return;
-
- StampIfMissing(shortcutPath);
- }
- catch (Exception ex)
- {
- Logger.Warn("Could not stamp AUMID on Start Menu shortcut");
- Logger.Warn(ex);
- }
- }
-
- private static string? ResolveStartMenuShortcut()
- {
- // {autostartmenu} in Inno Setup resolves to the per-user Start Menu Programs folder.
- string baseDir = Environment.GetFolderPath(
- Environment.SpecialFolder.StartMenu, Environment.SpecialFolderOption.DoNotVerify);
- string userPath = Path.Combine(baseDir, "Programs", ShortcutName + ".lnk");
- if (File.Exists(userPath))
- return userPath;
-
- // Fallback to the common Start Menu Programs folder.
- string commonBase = Environment.GetFolderPath(
- Environment.SpecialFolder.CommonStartMenu, Environment.SpecialFolderOption.DoNotVerify);
- string commonPath = Path.Combine(commonBase, "Programs", ShortcutName + ".lnk");
- return File.Exists(commonPath) ? commonPath : null;
- }
-
- [UnconditionalSuppressMessage(
- "Trimming",
- "IL2072",
- Justification = "ShellLink COM activation uses the shell-registered CLSID and COM interfaces declared below; it does not depend on app-owned constructors trimmed from this assembly.")]
- private static void StampIfMissing(string shortcutPath)
- {
- // ShellLink (CLSID 00021401-0000-0000-C000-000000000046) implements IPersistFile and
- // IPropertyStore for .lnk files. We read the AppUserModel.Id value; if it already
- // matches, there's nothing to do, otherwise we set it and persist.
- var shellLinkClsid = new Guid("00021401-0000-0000-C000-000000000046");
- object? linkObj = null;
- try
- {
- linkObj = Activator.CreateInstance(Type.GetTypeFromCLSID(shellLinkClsid)
- ?? throw new InvalidOperationException("ShellLink CLSID is not registered"));
- if (linkObj is not IPersistFile persistFile)
- return;
-
- persistFile.Load(shortcutPath, 0);
-
- if (linkObj is not IPropertyStore props)
- return;
-
- if (TryGetAumid(props, out string? existing) && existing == Aumid)
- return; // Already correct.
-
- var aumidValue = new PropVariant(Aumid);
- try
- {
- props.SetValue(AppUserModelIdKey, aumidValue);
- props.Commit();
- persistFile.Save(shortcutPath, fRemember: false);
- }
- finally
- {
- aumidValue.Clear();
- }
- }
- finally
- {
- if (linkObj is not null)
- Marshal.ReleaseComObject(linkObj);
- }
- }
-
- private static bool TryGetAumid(IPropertyStore props, out string? value)
- {
- value = null;
- try
- {
- uint count = props.GetCount();
- for (uint i = 0; i < count; i++)
- {
- props.GetAt(i, out PropertyKey key);
- if (key.fmtid == AppUserModelIdKey.fmtid && key.pid == AppUserModelIdKey.pid)
- {
- var pv = new PropVariant();
- try
- {
- props.GetValue(key, out pv);
- value = pv.Value as string;
- return true;
- }
- finally
- {
- pv.Clear();
- }
- }
- }
- }
- catch
- {
- // Reading failed; fall through to write.
- }
- return false;
- }
-
- // ── COM interop ──────────────────────────────────────────────────────────
-
- [ComImport]
- [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
- [Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99")]
- internal interface IPropertyStore
- {
- uint GetCount();
- void GetAt(uint i, out PropertyKey key);
- void GetValue([In] ref PropertyKey key, out PropVariant value);
- void SetValue([In] ref PropertyKey key, [In] ref PropVariant value);
- void Commit();
- }
-
- [ComImport]
- [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
- [Guid("0000010B-0000-0000-C000-000000000046")]
- internal interface IPersistFile
- {
- void GetClassID(out Guid pClassID);
- void IsDirty();
- void Load([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, uint dwMode);
- void Save([MarshalAs(UnmanagedType.LPWStr)] string pszFileName, bool fRemember);
- void SaveCompleted([MarshalAs(UnmanagedType.LPWStr)] string pszFileName);
- void GetCurFile([MarshalAs(UnmanagedType.LPWStr)] out string ppszFileName);
- }
-
- [StructLayout(LayoutKind.Sequential)]
- internal struct PropertyKey
- {
- public Guid fmtid;
- public int pid;
- public PropertyKey(Guid fmtid, int pid)
- {
- this.fmtid = fmtid;
- this.pid = pid;
- }
- }
-
- // Minimal PropVariant supporting VT_LPWSTR (string) — the only type we set/read for AUMID.
- [StructLayout(LayoutKind.Sequential)]
- internal struct PropVariant
- {
- private ushort vt;
- private readonly ushort reserved1;
- private readonly ushort reserved2;
- private readonly ushort reserved3;
- private IntPtr value1;
- private readonly IntPtr value2;
-
- public PropVariant(string value)
- {
- vt = (ushort)VarEnum.VT_LPWSTR;
- reserved1 = reserved2 = reserved3 = 0;
- value1 = Marshal.StringToCoTaskMemUni(value);
- value2 = IntPtr.Zero;
- }
-
- public string? Value
- {
- get
- {
- if ((VarEnum)vt != VarEnum.VT_LPWSTR || value1 == IntPtr.Zero)
- return null;
- return Marshal.PtrToStringUni(value1);
- }
- }
-
- public void Clear()
- {
- if ((VarEnum)vt == VarEnum.VT_LPWSTR && value1 != IntPtr.Zero)
- Marshal.FreeCoTaskMem(value1);
- vt = 0;
- value1 = IntPtr.Zero;
- }
- }
-}
-#endif
diff --git a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaAppHost.cs b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaAppHost.cs
index fb417b108e..57a7722b87 100644
--- a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaAppHost.cs
+++ b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaAppHost.cs
@@ -72,12 +72,6 @@ Welcome to UniGetUI Version {CoreData.VersionName}
Logger.ImportantInfo($"Packaged (MSIX): {CoreTools.IsPackagedApp()}");
Logger.ImportantInfo($"Args: {(args.Length > 0 ? string.Join(" ", args) : "(none)")}");
- // Stamp the AUMID onto the Start Menu shortcut so the shell surfaces toasts. The
- // installer cannot write the property-store value directly; this is idempotent.
-#if WINDOWS
- AppShortcutAumidStamper.EnsureStamped();
-#endif
-
// Bind Avalonia's UI-thread dispatcher to this (main/STA) thread before the single-instance
// listener starts: if a second instance connects mid-startup, the listener's Dispatcher.UIThread.Post
// would otherwise bind it to the worker thread and make Win32Platform.Initialize throw.
diff --git a/src/UniGetUI.Avalonia/Infrastructure/Win32ToastNotifier.cs b/src/UniGetUI.Avalonia/Infrastructure/Win32ToastNotifier.cs
index 29b2205811..882d099612 100644
--- a/src/UniGetUI.Avalonia/Infrastructure/Win32ToastNotifier.cs
+++ b/src/UniGetUI.Avalonia/Infrastructure/Win32ToastNotifier.cs
@@ -14,7 +14,7 @@ namespace UniGetUI.Avalonia.Infrastructure;
///
/// The notifier is keyed by an AppUserModelID (AUMID). For an unpackaged Win32 app the
/// shell will only surface toasts for that AUMID once a Start Menu shortcut carrying the
-/// same AUMID exists; handles that side. Toast
+/// same AUMID exists; the installer writes that property on its Start Menu shortcut. Toast
/// activation (button clicks) is intentionally not wired — clicking the toast launches
/// the app with its unigetui:// deep-link so the single-instance handler can
/// foreground the window, which keeps this dependency-free.
diff --git a/src/UniGetUI.Avalonia/Infrastructure/WindowsAvaloniaRenderingPolicy.cs b/src/UniGetUI.Avalonia/Infrastructure/WindowsAvaloniaRenderingPolicy.cs
index 948a097231..e029da22ca 100644
--- a/src/UniGetUI.Avalonia/Infrastructure/WindowsAvaloniaRenderingPolicy.cs
+++ b/src/UniGetUI.Avalonia/Infrastructure/WindowsAvaloniaRenderingPolicy.cs
@@ -1,7 +1,6 @@
using System.Diagnostics;
-using System.Diagnostics.CodeAnalysis;
-using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
+using System.Runtime.InteropServices.Marshalling;
using System.Runtime.Versioning;
using Avalonia.Controls;
using UniGetUI.Core.Logging;
@@ -9,10 +8,11 @@
namespace UniGetUI.Avalonia.Infrastructure;
-internal static class WindowsAvaloniaRenderingPolicy
+internal static partial class WindowsAvaloniaRenderingPolicy
{
private static bool? _hasHardwareGpu;
private static bool? _shouldUseSoftwareRendering;
+ private static readonly StrategyBasedComWrappers ComWrappers = new();
public static bool ShouldUseSoftwareRendering
{
@@ -60,23 +60,13 @@ private static bool HasHardwareGpu
}
[SupportedOSPlatform("windows")]
- [UnconditionalSuppressMessage("Trimming", "IL2050",
- Justification = "The DXGI COM interfaces are declared in full and referenced directly here, so their members are preserved by trimming.")]
private static bool DetectHardwareGpu()
{
- if (!RuntimeFeature.IsDynamicCodeSupported)
- {
- Logger.Info(
- "Skipping DXGI hardware GPU detection because NativeAOT does not support legacy COM activation."
- );
- return true;
- }
-
try
{
Guid factoryIid = typeof(IDXGIFactory1).GUID;
- if (CreateDXGIFactory1(ref factoryIid, out object factoryObj) != HResult.Ok
- || factoryObj is not IDXGIFactory1 factory)
+ if (CreateDXGIFactory1(ref factoryIid, out nint nativeFactory) != HResult.Ok
+ || nativeFactory == IntPtr.Zero)
{
Logger.Warn("Could not create DXGI factory; assuming a hardware GPU is present.");
return true;
@@ -84,37 +74,51 @@ private static bool DetectHardwareGpu()
try
{
+ var factory = (IDXGIFactory1)ComWrappers.GetOrCreateObjectForComInstance(
+ nativeFactory,
+ CreateObjectFlags.None
+ );
for (uint i = 0; ; i++)
{
- int hr = factory.EnumAdapters1(i, out IDXGIAdapter1 adapter);
- if (hr == HResult.DxgiErrorNotFound || hr != HResult.Ok || adapter is null)
+ int hr = factory.EnumAdapters1(i, out nint nativeAdapter);
+ if (hr == HResult.DxgiErrorNotFound || hr != HResult.Ok || nativeAdapter == IntPtr.Zero)
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;
+ var adapter = (IDXGIAdapter1)ComWrappers.GetOrCreateObjectForComInstance(
+ nativeAdapter,
+ CreateObjectFlags.None
+ );
+ unsafe
+ {
+ DXGI_ADAPTER_DESC1 desc = default;
+ if (adapter.GetDesc1((nint)(&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);
+ Marshal.Release(nativeAdapter);
}
}
+
+ return false;
}
finally
{
- Marshal.ReleaseComObject(factory);
+ Marshal.Release(nativeFactory);
}
-
- return false;
}
catch (Exception ex)
{
@@ -124,10 +128,10 @@ private static bool DetectHardwareGpu()
}
}
- [DllImport("dxgi.dll", ExactSpelling = true)]
- private static extern int CreateDXGIFactory1(
+ [LibraryImport("dxgi.dll", EntryPoint = "CreateDXGIFactory1")]
+ private static partial int CreateDXGIFactory1(
ref Guid riid,
- [MarshalAs(UnmanagedType.IUnknown)] out object ppFactory
+ out nint factory
);
private static class HResult
@@ -142,60 +146,106 @@ private static class HResult
private const uint MicrosoftVendorId = 0x1414;
private const uint BasicRenderDriverDeviceId = 0x8C;
- [ComImport]
+ [GeneratedComInterface]
[Guid("770aae78-f26f-4dba-a829-253c83d1b387")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
- private interface IDXGIFactory1
+ internal partial interface IDXGIFactory1
{
- void SetPrivateData();
- void SetPrivateDataInterface();
- void GetPrivateData();
- void GetParent();
- void EnumAdapters();
- void MakeWindowAssociation();
- void GetWindowAssociation();
- void CreateSwapChain();
- void CreateSoftwareAdapter();
+ [PreserveSig]
+ int SetPrivateData(in Guid name, uint dataSize, nint data);
+
+ [PreserveSig]
+ int SetPrivateDataInterface(in Guid name, nint unknown);
+
+ [PreserveSig]
+ int GetPrivateData(in Guid name, ref uint dataSize, nint data);
+
+ [PreserveSig]
+ int GetParent(in Guid riid, out nint parent);
+
+ [PreserveSig]
+ int EnumAdapters(uint adapter, out nint adapterPointer);
+
+ [PreserveSig]
+ int MakeWindowAssociation(nint window, uint flags);
+
+ [PreserveSig]
+ int GetWindowAssociation(out nint window);
+
+ [PreserveSig]
+ int CreateSwapChain(nint device, nint description, out nint swapChain);
[PreserveSig]
- int EnumAdapters1(uint adapter, out IDXGIAdapter1 ppAdapter);
+ int CreateSoftwareAdapter(nint module, out nint adapter);
[PreserveSig]
- bool IsCurrent();
+ int EnumAdapters1(uint adapter, out nint adapterPointer);
+
+ [PreserveSig]
+ int IsCurrent();
}
- [ComImport]
+ [GeneratedComInterface]
[Guid("29038f61-3839-4626-91fd-086879011a05")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
- private interface IDXGIAdapter1
+ internal partial interface IDXGIAdapter1
{
- void SetPrivateData();
- void SetPrivateDataInterface();
- void GetPrivateData();
- void GetParent();
- void EnumOutputs();
- void GetDesc();
- void CheckInterfaceSupport();
+ [PreserveSig]
+ int SetPrivateData(in Guid name, uint dataSize, nint data);
+
+ [PreserveSig]
+ int SetPrivateDataInterface(in Guid name, nint unknown);
+
+ [PreserveSig]
+ int GetPrivateData(in Guid name, ref uint dataSize, nint data);
+
+ [PreserveSig]
+ int GetParent(in Guid riid, out nint parent);
+
+ [PreserveSig]
+ int EnumOutputs(uint output, out nint outputPointer);
+
+ [PreserveSig]
+ int GetDesc(out nint description);
[PreserveSig]
- int GetDesc1(out DXGI_ADAPTER_DESC1 pDesc);
+ int CheckInterfaceSupport(in Guid interfaceName, out long userModeVersion);
+
+ [PreserveSig]
+ int GetDesc1(nint description);
}
- [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
- private struct DXGI_ADAPTER_DESC1
+ [StructLayout(LayoutKind.Explicit, Size = 312)]
+ internal struct DXGI_ADAPTER_DESC1
{
- [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
- public string Description;
-
+ [FieldOffset(256)]
public uint VendorId;
+
+ [FieldOffset(260)]
public uint DeviceId;
+
+ [FieldOffset(264)]
public uint SubSysId;
+
+ [FieldOffset(268)]
public uint Revision;
- public UIntPtr DedicatedVideoMemory;
- public UIntPtr DedicatedSystemMemory;
- public UIntPtr SharedSystemMemory;
+
+ [FieldOffset(272)]
+ public nuint DedicatedVideoMemory;
+
+ [FieldOffset(280)]
+ public nuint DedicatedSystemMemory;
+
+ [FieldOffset(288)]
+ public nuint SharedSystemMemory;
+
+ [FieldOffset(296)]
public uint AdapterLuidLowPart;
+
+ [FieldOffset(300)]
public int AdapterLuidHighPart;
+
+ [FieldOffset(304)]
public uint Flags;
}
}
diff --git a/src/UniGetUI.Avalonia/Program.cs b/src/UniGetUI.Avalonia/Program.cs
index 755498923c..759efe43d9 100644
--- a/src/UniGetUI.Avalonia/Program.cs
+++ b/src/UniGetUI.Avalonia/Program.cs
@@ -27,7 +27,7 @@ public static void Main(string[] args)
#if WINDOWS
// Stamp the AUMID onto this process before anything else so the shell attributes
// Action-Center toasts to UniGetUI. Must match the AUMID stamped on the Start Menu
- // shortcut by AppShortcutAumidStamper.
+ // shortcut by the installer.
Win32ToastNotifier.SetProcessAumid();
#endif
diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
index b0875d7cc2..2bb37579d3 100644
--- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
+++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj
@@ -25,6 +25,7 @@
full
false
false
+ true
@@ -231,7 +232,6 @@
-
diff --git a/src/UniGetUI.Avalonia/Views/Controls/DataGridWheelAnimator.cs b/src/UniGetUI.Avalonia/Views/Controls/DataGridWheelAnimator.cs
index d9eb2f3138..398ad3ea6f 100644
--- a/src/UniGetUI.Avalonia/Views/Controls/DataGridWheelAnimator.cs
+++ b/src/UniGetUI.Avalonia/Views/Controls/DataGridWheelAnimator.cs
@@ -1,5 +1,5 @@
using System;
-using System.Reflection;
+using System.Runtime.CompilerServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
@@ -10,19 +10,15 @@ namespace UniGetUI.Avalonia.Views.Controls;
///
/// Eases DataGrid wheel scrolling to a stop (WinUI-like) instead of jumping the whole delta at once.
-/// The grid has no public scroll offset, so we drive its internal UpdateScroll via reflection; if that
-/// ever breaks we never attach and the stock instant behavior stands.
+/// The grid has no public scroll offset, so we statically bind its internal UpdateScroll method.
+/// The binding is NativeAOT-safe but must be revalidated when Avalonia DataGrid is upgraded.
///
public sealed class DataGridWheelAnimator
{
- private static readonly MethodInfo? UpdateScroll =
- typeof(DataGrid).GetMethod("UpdateScroll", BindingFlags.Instance | BindingFlags.NonPublic);
-
private const double WheelStep = 70.0; // pixels travelled per notch; larger = faster scroll, more to glide over
private const double Tau = 0.12; // ease time constant in seconds; larger = longer, more visible glide
private readonly DataGrid _grid;
- private readonly object?[] _args = new object?[1];
private double _pending;
private TimeSpan? _lastFrame;
private bool _frameRequested;
@@ -35,7 +31,7 @@ private DataGridWheelAnimator(DataGrid grid)
public static void Attach(DataGrid grid)
{
- if (UpdateScroll is not null) _ = new DataGridWheelAnimator(grid);
+ _ = new DataGridWheelAnimator(grid);
}
private void OnWheel(object? sender, PointerWheelEventArgs e)
@@ -70,11 +66,13 @@ private void OnFrame(TimeSpan now)
double remaining = _pending * Math.Exp(-dt / Tau);
double step = Math.Abs(remaining) < 0.5 ? _pending : _pending - remaining;
- _args[0] = new Vector(0, step);
- bool scrolled = UpdateScroll!.Invoke(_grid, _args) is true;
+ bool scrolled = UpdateScroll(_grid, new Vector(0, step));
_pending -= step;
if (!scrolled) { _pending = 0; return; }
if (_pending != 0) RequestFrame();
}
+
+ [UnsafeAccessor(UnsafeAccessorKind.Method, Name = "UpdateScroll")]
+ private static extern bool UpdateScroll(DataGrid grid, Vector offset);
}