diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 8ab64e519c5..b575db6cddd 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1411,7 +1411,7 @@ public static extern IntPtr CreateIconFromResourceEx(byte* pbIconBits, uint cbIc public static extern IntPtr SetCapture(IntPtr hWnd); [DllImport("user32.dll")] - public static extern IntPtr SetTimer(IntPtr hWnd, IntPtr nIDEvent, uint uElapse, TimerProc lpTimerFunc); + public static extern IntPtr SetTimer(IntPtr hWnd, IntPtr nIDEvent, uint uElapse, TimerProc? lpTimerFunc); [DllImport("user32.dll", SetLastError = true)] public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint); [DllImport("user32.dll")] diff --git a/src/Windows/Avalonia.Win32/OffscreenParentWindow.cs b/src/Windows/Avalonia.Win32/OffscreenParentWindow.cs index 7253b3c0ed7..2baf1a3de7c 100644 --- a/src/Windows/Avalonia.Win32/OffscreenParentWindow.cs +++ b/src/Windows/Avalonia.Win32/OffscreenParentWindow.cs @@ -1,59 +1,10 @@ using System; -using System.ComponentModel; -using System.Runtime.InteropServices; -using Avalonia.Win32.Interop; + namespace Avalonia.Win32 { internal class OffscreenParentWindow { - public static IntPtr Handle { get; } = CreateParentWindow(); - - private static UnmanagedMethods.WndProc? s_wndProcDelegate; - - private static IntPtr CreateParentWindow() - { - s_wndProcDelegate = ParentWndProc; - - var wndClassEx = new UnmanagedMethods.WNDCLASSEX - { - cbSize = Marshal.SizeOf(), - hInstance = UnmanagedMethods.GetModuleHandle(null), - lpfnWndProc = s_wndProcDelegate, - lpszClassName = "AvaloniaEmbeddedWindow-" + Guid.NewGuid(), - }; - - var atom = UnmanagedMethods.RegisterClassEx(ref wndClassEx); - - if (atom == 0) - { - throw new Win32Exception(); - } - - var hwnd = UnmanagedMethods.CreateWindowEx( - 0, - atom, - null, - (int)UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW, - UnmanagedMethods.CW_USEDEFAULT, - UnmanagedMethods.CW_USEDEFAULT, - UnmanagedMethods.CW_USEDEFAULT, - UnmanagedMethods.CW_USEDEFAULT, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero, - IntPtr.Zero); - - if (hwnd == IntPtr.Zero) - { - throw new Win32Exception(); - } - - return hwnd; - } - - private static IntPtr ParentWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) - { - return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); - } + private static SimpleWindow s_simpleWindow = new(null); + public static IntPtr Handle { get; } = s_simpleWindow.Handle; } } diff --git a/src/Windows/Avalonia.Win32/SimpleWindow.cs b/src/Windows/Avalonia.Win32/SimpleWindow.cs new file mode 100644 index 00000000000..df1f3df68a6 --- /dev/null +++ b/src/Windows/Avalonia.Win32/SimpleWindow.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Concurrent; +using System.ComponentModel; +using System.Runtime.InteropServices; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32; + +internal class SimpleWindow : IDisposable +{ + private readonly UnmanagedMethods.WndProc? _wndProc; + private static UnmanagedMethods.WndProc s_wndProcDelegate; + public IntPtr Handle { get; private set; } + private static string s_className; + private static uint s_classAtom; + private static ConcurrentDictionary s_Instances = new(); + + static SimpleWindow() + { + s_wndProcDelegate = WndProc; + var wndClassEx = new UnmanagedMethods.WNDCLASSEX + { + cbSize = Marshal.SizeOf(), + hInstance = UnmanagedMethods.GetModuleHandle(null), + lpfnWndProc = s_wndProcDelegate, + lpszClassName = s_className = "AvaloniaSimpleWindow-" + Guid.NewGuid(), + }; + + s_classAtom = UnmanagedMethods.RegisterClassEx(ref wndClassEx); + } + + public SimpleWindow(UnmanagedMethods.WndProc? wndProc) + { + _wndProc = wndProc; + var handle = GCHandle.Alloc(this); + try + { + var hwnd = UnmanagedMethods.CreateWindowEx( + 0, + s_classAtom, + null, + (int)UnmanagedMethods.WindowStyles.WS_OVERLAPPEDWINDOW, + UnmanagedMethods.CW_USEDEFAULT, + UnmanagedMethods.CW_USEDEFAULT, + UnmanagedMethods.CW_USEDEFAULT, + UnmanagedMethods.CW_USEDEFAULT, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero, + GCHandle.ToIntPtr(handle)); + if (hwnd == IntPtr.Zero) + { + throw new Win32Exception(); + } + + Handle = hwnd; + } + finally + { + handle.Free(); + } + } + + private static IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + SimpleWindow? window; + if (msg == (uint)UnmanagedMethods.WindowsMessage.WM_CREATE) + { + var handle = Marshal.ReadIntPtr(lParam); + window = (SimpleWindow?)GCHandle.FromIntPtr(handle).Target; + if (window == null) + return IntPtr.Zero; + s_Instances.TryAdd(hWnd, window); + } + else + { + s_Instances.TryGetValue(hWnd, out window); + } + + if (msg == (uint)UnmanagedMethods.WindowsMessage.WM_DESTROY) + s_Instances.TryRemove(hWnd, out _); + + return window?._wndProc?.Invoke(hWnd, msg, wParam, lParam) + ?? UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); + } + + public void Dispose() + { + UnmanagedMethods.DestroyWindow(Handle); + Handle = IntPtr.Zero; + } +} diff --git a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs index 5255eaff980..5a8360b515a 100644 --- a/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs +++ b/src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositorConnection.cs @@ -8,6 +8,7 @@ using Avalonia.OpenGL.Egl; using Avalonia.Rendering; using Avalonia.Win32.Interop; +using MicroCom.Runtime; namespace Avalonia.Win32.WinRT.Composition; @@ -80,18 +81,65 @@ private class RunLoopHandler : CallbackBase, IAsyncActionCompletedHandler { private readonly WinUiCompositorConnection _parent; private readonly Stopwatch _st = Stopwatch.StartNew(); - + private TimeSpan? _commitDueAt; + private IAsyncAction? _currentCommit; public RunLoopHandler(WinUiCompositorConnection parent) { _parent = parent; } - - public void Invoke(IAsyncAction asyncInfo, AsyncStatus asyncStatus) + + public void Invoke(IAsyncAction? asyncInfo, AsyncStatus asyncStatus) { + if (_currentCommit == null || + _currentCommit.GetNativeIntPtr() != asyncInfo.GetNativeIntPtr()) + return; + OnCommitCompleted(); + } + + private void OnCommitCompleted() + { + _currentCommit?.Dispose(); + _currentCommit = null; _parent.Tick?.Invoke(_st.Elapsed); - using var act = _parent._shared.Compositor5.RequestCommitAsync(); - act.SetCompleted(this); + ScheduleNextCommit(); + } + + private void ScheduleNextCommit() + { + lock (_parent._shared.SyncRoot) + { + _commitDueAt = _st.Elapsed + TimeSpan.FromSeconds(1); + _currentCommit = _parent._shared.Compositor5.RequestCommitAsync(); + _currentCommit.SetCompleted(this); + } } + + public void WatchDog() + { + // This is a workaround for a nasty WinUI composition API bug that prevents + // RequestCommitAsync to ever complete after D3D device loss event with some systems + // (A notable example is after pause/resume in Parallels Desktop) + // We check if we haven't got a commit completion callback for a second + // And forcefully trigger the next one, which makes the entire thing to unstuck + + if (_st.Elapsed > _commitDueAt && _currentCommit != null) + { + Logger.TryGet(LogEventLevel.Error, LogArea.Visual)?.Log(this, + "windows::UI::Composition::ICompositor5.RequestCommitAsync timed out, force-triggering next tick"); + try + { + _currentCommit?.GetResults(); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, LogArea.Visual)?.Log(this, + "ICompositor5::RequestCommitAsync failed: {HR}, {ERR}", e.HResult, e.ToString()); + } + OnCommitCompleted(); + } + } + + public void Start() => ScheduleNextCommit(); } private void RunLoop() @@ -100,10 +148,19 @@ private void RunLoop() AppDomain.CurrentDomain.ProcessExit += (_, _) => cts.Cancel(); - lock (_shared.SyncRoot) - using (var act = _shared.Compositor5.RequestCommitAsync()) - act.SetCompleted(new RunLoopHandler(this)); + var handler = new RunLoopHandler(this); + handler.Start(); + using var dw = new SimpleWindow((hwnd, msg, w, l) => + { + if (msg == (uint)UnmanagedMethods.WindowsMessage.WM_TIMER) + { + handler.WatchDog(); + UnmanagedMethods.SetTimer(hwnd, IntPtr.Zero, 1000, null); + } + return UnmanagedMethods.DefWindowProc(hwnd, msg, w, l); + }); + UnmanagedMethods.SetTimer(dw.Handle, IntPtr.Zero, 1000, null); while (!cts.IsCancellationRequested) { UnmanagedMethods.GetMessage(out var msg, IntPtr.Zero, 0, 0);