Skip to content

Commit

Permalink
[WinRT] Added watchdog for ICompositor5::RequestCommitAsync (#16393)
Browse files Browse the repository at this point in the history
  • Loading branch information
kekekeks authored Jul 24, 2024
1 parent 189f95e commit e0f0e90
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 61 deletions.
2 changes: 1 addition & 1 deletion src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down
55 changes: 3 additions & 52 deletions src/Windows/Avalonia.Win32/OffscreenParentWindow.cs
Original file line number Diff line number Diff line change
@@ -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<UnmanagedMethods.WNDCLASSEX>(),
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;
}
}
92 changes: 92 additions & 0 deletions src/Windows/Avalonia.Win32/SimpleWindow.cs
Original file line number Diff line number Diff line change
@@ -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<IntPtr, SimpleWindow> s_Instances = new();

static SimpleWindow()
{
s_wndProcDelegate = WndProc;
var wndClassEx = new UnmanagedMethods.WNDCLASSEX
{
cbSize = Marshal.SizeOf<UnmanagedMethods.WNDCLASSEX>(),
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Avalonia.OpenGL.Egl;
using Avalonia.Rendering;
using Avalonia.Win32.Interop;
using MicroCom.Runtime;

namespace Avalonia.Win32.WinRT.Composition;

Expand Down Expand Up @@ -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()
Expand All @@ -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);
Expand Down

0 comments on commit e0f0e90

Please sign in to comment.