Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WinRT] Added watchdog for ICompositor5::RequestCommitAsync #16393

Merged
merged 1 commit into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not really descriptive. Having different classnames for offscreen and winui-hack windows might help somebody's in the future.

Copy link
Member Author

@kekekeks kekekeks Jul 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The window class is now reused for all windows to avoid wasting resources for no reason. It's not possible to customize it per-window anymore

};

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
Loading