From 263baaf80e95ba9610bf651e4394e12e5030f65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Moreau?= Date: Tue, 14 Jul 2026 15:11:58 -0400 Subject: [PATCH] Restore NativeAOT desktop features Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- UniGetUI.iss | 2 +- .../Infrastructure/AppShortcutAumidStamper.cs | 226 ------------------ .../Infrastructure/AvaloniaAppHost.cs | 6 - .../Infrastructure/Win32ToastNotifier.cs | 2 +- .../WindowsAvaloniaRenderingPolicy.cs | 180 +++++++++----- src/UniGetUI.Avalonia/Program.cs | 2 +- .../UniGetUI.Avalonia.csproj | 2 +- .../Views/Controls/DataGridWheelAnimator.cs | 18 +- 8 files changed, 127 insertions(+), 311 deletions(-) delete mode 100644 src/UniGetUI.Avalonia/Infrastructure/AppShortcutAumidStamper.cs 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); }