Handle quit and window teardown flow

Adds Win32k quit-message support via `NtUserPostQuitMessage` and queue handling so `WM_QUIT` is surfaced through message APIs and wait checks. Improves window teardown by posting `WM_DESTROY`/`WM_NCDESTROY`, retaining destroyed windows briefly for dispatch, and forgetting them after `WM_NCDESTROY`. Host close handling is unified with `HostEventQueue.RequestClose`/`Reset`, and process termination now hides the desktop window before stopping emulation.

This also fixes a bug in Vulkan rendering where it assumed the window still existed and crashed with an access violation.
This commit is contained in:
AdvDebug
2026-07-24 16:27:34 +03:00
parent 830da7227a
commit 290c02018c
8 changed files with 195 additions and 90 deletions
@@ -369,7 +369,6 @@ namespace Brovan.Core.Emulation.OS.SharedHelpers
private const uint WM_SIZE = 0x0005;
private const uint WM_SETFOCUS = 0x0007;
private const uint WM_KILLFOCUS = 0x0008;
private const uint WM_CLOSE = 0x0010;
private const uint WM_KEYDOWN = 0x0100;
private const uint WM_KEYUP = 0x0101;
private const uint WM_SYSKEYDOWN = 0x0104;
@@ -537,57 +536,57 @@ namespace Brovan.Core.Emulation.OS.SharedHelpers
{
case X11.KeyPress:
case X11.KeyRelease:
{
ref X11.XKeyEvent key = ref Unsafe.As<X11.XEvent, X11.XKeyEvent>(ref nativeEvent);
_modifierState = key.State;
{
ref X11.XKeyEvent key = ref Unsafe.As<X11.XEvent, X11.XKeyEvent>(ref nativeEvent);
_modifierState = key.State;
uint virtualKey = KeysymToVirtualKey(LookupKeysym((byte)key.Keycode));
if (virtualKey == 0)
uint virtualKey = KeysymToVirtualKey(LookupKeysym((byte)key.Keycode));
if (virtualKey == 0)
return;
bool down = nativeEvent.Type == X11.KeyPress;
bool altHeld = (key.State & X11.Mod1Mask) != 0;
bool system = altHeld || virtualKey == VK_MENU || virtualKey == VK_F10;
uint message = down
? (system ? WM_SYSKEYDOWN : WM_KEYDOWN)
: (system ? WM_SYSKEYUP : WM_KEYUP);
HostEventQueue.Enqueue(message, virtualKey, BuildKeyLParam(key.Keycode, virtualKey, down, altHeld));
return;
bool down = nativeEvent.Type == X11.KeyPress;
bool altHeld = (key.State & X11.Mod1Mask) != 0;
bool system = altHeld || virtualKey == VK_MENU || virtualKey == VK_F10;
uint message = down
? (system ? WM_SYSKEYDOWN : WM_KEYDOWN)
: (system ? WM_SYSKEYUP : WM_KEYUP);
HostEventQueue.Enqueue(message, virtualKey, BuildKeyLParam(key.Keycode, virtualKey, down, altHeld));
return;
}
}
case X11.ButtonPress:
case X11.ButtonRelease:
{
ref X11.XButtonEvent button = ref Unsafe.As<X11.XEvent, X11.XButtonEvent>(ref nativeEvent);
_modifierState = button.State;
TranslateButton(ref button, nativeEvent.Type == X11.ButtonPress);
return;
}
{
ref X11.XButtonEvent button = ref Unsafe.As<X11.XEvent, X11.XButtonEvent>(ref nativeEvent);
_modifierState = button.State;
TranslateButton(ref button, nativeEvent.Type == X11.ButtonPress);
return;
}
case X11.MotionNotify:
{
ref X11.XMotionEvent motion = ref Unsafe.As<X11.XEvent, X11.XMotionEvent>(ref nativeEvent);
_modifierState = motion.State;
HostEventQueue.Enqueue(WM_MOUSEMOVE, StateToMouseKeys(motion.State), MakeLParam(motion.X, motion.Y));
return;
}
{
ref X11.XMotionEvent motion = ref Unsafe.As<X11.XEvent, X11.XMotionEvent>(ref nativeEvent);
_modifierState = motion.State;
HostEventQueue.Enqueue(WM_MOUSEMOVE, StateToMouseKeys(motion.State), MakeLParam(motion.X, motion.Y));
return;
}
case X11.Expose:
HostEventQueue.MarkRepaint();
return;
case X11.ConfigureNotify:
{
ref X11.XConfigureEvent configure = ref Unsafe.As<X11.XEvent, X11.XConfigureEvent>(ref nativeEvent);
if (TryGetWindow(configure.Window, out LinuxWindow? window))
window?.OnConfigured(configure.Width, configure.Height);
{
ref X11.XConfigureEvent configure = ref Unsafe.As<X11.XEvent, X11.XConfigureEvent>(ref nativeEvent);
if (TryGetWindow(configure.Window, out LinuxWindow? window))
window?.OnConfigured(configure.Width, configure.Height);
HostEventQueue.Enqueue(WM_SIZE, 0, MakeLParam(configure.Width, configure.Height));
HostEventQueue.MarkRepaint();
return;
}
HostEventQueue.Enqueue(WM_SIZE, 0, MakeLParam(configure.Width, configure.Height));
HostEventQueue.MarkRepaint();
return;
}
case X11.FocusIn:
HostEventQueue.Enqueue(WM_SETFOCUS, 0, 0);
@@ -598,18 +597,13 @@ namespace Brovan.Core.Emulation.OS.SharedHelpers
return;
case X11.ClientMessage:
{
ref X11.XClientMessageEvent client = ref Unsafe.As<X11.XEvent, X11.XClientMessageEvent>(ref nativeEvent);
if (client.MessageType == _wmProtocols && client.Data0 == _wmDeleteWindow)
{
HostEventQueue.Enqueue(WM_CLOSE, 0, 0);
ref X11.XClientMessageEvent client = ref Unsafe.As<X11.XEvent, X11.XClientMessageEvent>(ref nativeEvent);
if (client.MessageType == _wmProtocols && client.Data0 == _wmDeleteWindow)
HostEventQueue.RequestClose();
if (TryGetWindow(client.Window, out LinuxWindow? window))
window?.Dispose();
return;
}
return;
}
}
}
@@ -1120,54 +1114,54 @@ namespace Brovan.Core.Emulation.OS.SharedHelpers
break;
case GdiPrimitiveKind.Ellipse:
{
Normalize(primitive, out int x, out int y, out uint width, out uint height);
if (primitive.HasBrush)
{
X11.XSetForeground(_xDisplay, gc, ResolvePixel(primitive.Brush.ColorRef));
X11.XFillArc(_xDisplay, windowHandle, gc, x, y, width, height, 0, 360 * 64);
}
Normalize(primitive, out int x, out int y, out uint width, out uint height);
if (primitive.HasBrush)
{
X11.XSetForeground(_xDisplay, gc, ResolvePixel(primitive.Brush.ColorRef));
X11.XFillArc(_xDisplay, windowHandle, gc, x, y, width, height, 0, 360 * 64);
}
if (primitive.HasPen)
{
X11.XSetForeground(_xDisplay, gc, ResolvePixel(primitive.Pen.ColorRef));
X11.XDrawArc(_xDisplay, windowHandle, gc, x, y, width, height, 0, 360 * 64);
}
if (primitive.HasPen)
{
X11.XSetForeground(_xDisplay, gc, ResolvePixel(primitive.Pen.ColorRef));
X11.XDrawArc(_xDisplay, windowHandle, gc, x, y, width, height, 0, 360 * 64);
}
break;
}
break;
}
case GdiPrimitiveKind.Polygon:
{
if (primitive.Points == null || primitive.Points.Length == 0)
{
if (primitive.Points == null || primitive.Points.Length == 0)
break;
X11.XPoint[] points = ToXPoints(primitive.Points, close: true);
if (primitive.HasBrush)
{
X11.XSetForeground(_xDisplay, gc, ResolvePixel(primitive.Brush.ColorRef));
X11.XFillPolygon(_xDisplay, windowHandle, gc, points, primitive.Points.Length, X11.Complex, X11.CoordModeOrigin);
}
if (primitive.HasPen)
{
X11.XSetForeground(_xDisplay, gc, ResolvePixel(primitive.Pen.ColorRef));
X11.XDrawLines(_xDisplay, windowHandle, gc, points, points.Length, X11.CoordModeOrigin);
}
break;
X11.XPoint[] points = ToXPoints(primitive.Points, close: true);
if (primitive.HasBrush)
{
X11.XSetForeground(_xDisplay, gc, ResolvePixel(primitive.Brush.ColorRef));
X11.XFillPolygon(_xDisplay, windowHandle, gc, points, primitive.Points.Length, X11.Complex, X11.CoordModeOrigin);
}
if (primitive.HasPen)
{
X11.XSetForeground(_xDisplay, gc, ResolvePixel(primitive.Pen.ColorRef));
X11.XDrawLines(_xDisplay, windowHandle, gc, points, points.Length, X11.CoordModeOrigin);
}
break;
}
case GdiPrimitiveKind.Polyline:
{
if (primitive.Points == null || primitive.Points.Length == 0)
break;
{
if (primitive.Points == null || primitive.Points.Length == 0)
break;
X11.XPoint[] points = ToXPoints(primitive.Points, close: false);
X11.XSetForeground(_xDisplay, gc, ResolvePixel(primitive.Pen.ColorRef));
X11.XDrawLines(_xDisplay, windowHandle, gc, points, points.Length, X11.CoordModeOrigin);
break;
}
X11.XPoint[] points = ToXPoints(primitive.Points, close: false);
X11.XSetForeground(_xDisplay, gc, ResolvePixel(primitive.Pen.ColorRef));
X11.XDrawLines(_xDisplay, windowHandle, gc, points, points.Length, X11.CoordModeOrigin);
break;
}
}
X11.XFlush(_xDisplay);
@@ -8,9 +8,29 @@ namespace Brovan.Core.Emulation.OS.SharedHelpers
{
internal static class HostEventQueue
{
private const uint WM_CLOSE = 0x0010;
private static int _pendingRepaint;
private static int _closeRequested;
private static readonly ConcurrentQueue<(uint Message, ulong WParam, ulong LParam)> PendingInput = new();
public static void RequestClose()
{
if (Interlocked.Exchange(ref _closeRequested, 1) != 0)
Environment.Exit(0);
Enqueue(WM_CLOSE, 0, 0);
}
public static void Reset()
{
Interlocked.Exchange(ref _closeRequested, 0);
Interlocked.Exchange(ref _pendingRepaint, 0);
while (PendingInput.TryDequeue(out _))
{
}
}
public static void MarkRepaint()
{
Interlocked.Exchange(ref _pendingRepaint, 1);
@@ -743,8 +743,7 @@ namespace Brovan.Core.Emulation.OS.SharedHelpers
{
if (msg == WM_CLOSE)
{
HostEventQueue.Enqueue(msg, 0UL, 0UL);
Close();
HostEventQueue.RequestClose();
return IntPtr.Zero;
}
@@ -40,6 +40,7 @@ namespace Brovan.Core.Emulation.OS.Windows
ProcessThreads.ExitCode = (int)ExitCode;
Instance.WinHelper.ClearTerminationState(ProcessThreads);
}
Instance.WinHelper.HideDesktopWindow();
Instance.StopEmulation();
return NTSTATUS.STATUS_SUCCESS;
}
@@ -63,6 +64,7 @@ namespace Brovan.Core.Emulation.OS.Windows
ProcessThreads.ExitCode = (int)ExitCode;
Instance.WinHelper.ClearTerminationState(ProcessThreads);
}
Instance.WinHelper.HideDesktopWindow();
Instance.StopEmulation();
return NTSTATUS.STATUS_SUCCESS;
}
@@ -15,9 +15,18 @@ namespace Brovan.Core.Emulation.OS.Windows.Win32k
WinWindow Window = Message.Hwnd == 0 ? null : Instance.WinHelper.GetWindow(Message.Hwnd);
if (Message.Hwnd != 0 && Window == null)
{
Instance.SetLastWinError(Win32kHelper.ERROR_INVALID_WINDOW_HANDLE);
Instance.SetRawSyscallReturn(0);
return NTSTATUS.STATUS_SUCCESS;
bool Teardown = Message.Message == Win32kHelper.WM_DESTROY || Message.Message == Win32kHelper.WM_NCDESTROY;
Window = Teardown ? Instance.WinHelper.GetDestroyedWindow(Message.Hwnd) : null;
if (Window == null)
{
Instance.SetLastWinError(Win32kHelper.ERROR_INVALID_WINDOW_HANDLE);
Instance.SetRawSyscallReturn(0);
return NTSTATUS.STATUS_SUCCESS;
}
if (Message.Message == Win32kHelper.WM_NCDESTROY)
Instance.WinHelper.ForgetDestroyedWindow(Message.Hwnd);
}
if (Window == null || Window.WndProc == 0)
@@ -0,0 +1,18 @@
using static Brovan.Core.Helpers.BinaryHelpers;
namespace Brovan.Core.Emulation.OS.Windows.Win32k
{
internal class NtUserPostQuitMessage : IWinSyscall
{
public NTSTATUS Handle(BinaryEmulator Instance)
{
ulong ExitCode = Instance.WinHelper.GetArg(0);
Win32kHelper.PostQuitMessage(Instance, ExitCode);
Instance.SetLastWinError(0);
Instance.SetBooleanSyscallReturn(true);
return NTSTATUS.STATUS_SUCCESS;
}
}
}
@@ -54,6 +54,7 @@ namespace Brovan.Core.Emulation.OS.Windows.Win32k
internal const uint WM_GETTEXT = 0x000D;
internal const uint WM_GETTEXTLENGTH = 0x000E;
internal const uint WM_NCHITTEST = 0x0084;
internal const uint WM_NCDESTROY = 0x0082;
internal const uint WM_PAINT = 0x000F;
internal const uint WM_SETTEXT = 0x000C;
internal const uint WM_KEYDOWN = 0x0100;
@@ -102,6 +103,8 @@ namespace Brovan.Core.Emulation.OS.Windows.Win32k
public readonly Dictionary<ulong, Win32kPenBrush> PenBrushObjects = new();
public ulong NextDeviceContext = FirstDeviceContextHandle;
public ulong CaptureWindow;
public bool QuitPosted;
public ulong QuitExitCode;
}
private sealed class Win32kDeviceContext
@@ -248,6 +251,13 @@ namespace Brovan.Core.Emulation.OS.Windows.Win32k
return true;
}
internal static void PostQuitMessage(BinaryEmulator Instance, ulong ExitCode)
{
Win32kState State = GetState(Instance);
State.QuitExitCode = ExitCode;
State.QuitPosted = true;
}
internal static bool TryGetMessage(BinaryEmulator Instance, ulong HwndFilter, uint MinMessage, uint MaxMessage, bool Remove, out Win32kMessage Message)
{
DrainHostEvents(Instance);
@@ -267,6 +277,14 @@ namespace Brovan.Core.Emulation.OS.Windows.Win32k
Index++;
}
if (State.QuitPosted)
{
Message = new Win32kMessage(0, WM_QUIT, State.QuitExitCode, 0, unchecked((uint)Instance.EmulatedTickCount64), 0, 0);
if (Remove)
State.QuitPosted = false;
return true;
}
Message = default;
return false;
}
@@ -279,6 +297,9 @@ namespace Brovan.Core.Emulation.OS.Windows.Win32k
return false;
Win32kState State = GetState(Instance);
if (State.QuitPosted && (WakeMask & QS_POSTMESSAGE) != 0)
return true;
foreach (Win32kMessage Candidate in State.MessageQueue)
{
if ((GetMessageWakeBits(Candidate.Message) & WakeMask) != 0)
@@ -1165,6 +1165,9 @@ namespace Brovan.Core.Emulation.OS.Windows
public ulong EtwNotificationEventHandle;
internal PebLdrTracker LdrTracker;
public readonly Dictionary<ulong, WinWindow> WinWindows = new();
private const int MaxDestroyedWindows = 64;
private readonly Dictionary<ulong, WinWindow> DestroyedWindows = new();
private readonly Queue<ulong> DestroyedWindowOrder = new();
public readonly Dictionary<ushort, WinWindowClass> WinWindowClassesByAtom = new();
private readonly Dictionary<string, ushort> WinWindowClassAtomsByKey = new(StringComparer.OrdinalIgnoreCase);
private ushort NextWindowClassAtom = 0xC000;
@@ -3863,6 +3866,14 @@ namespace Brovan.Core.Emulation.OS.Windows
{
}
public void HideDesktopWindow()
{
if (DesktopWindow == null || DesktopDisplay is not GuiThreadManager guiManager)
return;
guiManager.EnqueuePresent(DesktopWindowTitle, 0, 0, false, WindowState.Normal);
}
/// <summary>
/// Presents the current foreground Win32k window through the host window manager.
/// </summary>
@@ -3931,6 +3942,7 @@ namespace Brovan.Core.Emulation.OS.Windows
if (DesktopDisplay == null)
return;
HostEventQueue.Reset();
DesktopWindow = DesktopDisplay.CreateWindow(new WindowOptions
{
Title = DesktopWindowTitle,
@@ -4051,11 +4063,15 @@ namespace Brovan.Core.Emulation.OS.Windows
if (FocusWindow == Hwnd)
FocusWindow = 0;
Win32kHelper.PostMessage(Emulator, Hwnd, Win32kHelper.WM_DESTROY, 0, 0);
foreach (ulong Child in Window.Children.ToArray())
{
DestroyWindow(Child);
}
Win32kHelper.PostMessage(Emulator, Hwnd, Win32kHelper.WM_NCDESTROY, 0, 0);
if (Window.ParentHwnd != 0 && WinWindows.TryGetValue(Window.ParentHwnd, out WinWindow Parent))
{
Parent.Children.Remove(Hwnd);
@@ -4068,10 +4084,36 @@ namespace Brovan.Core.Emulation.OS.Windows
Window.Destroyed = true;
ClearUserWindowHandleEntry(Window);
WinWindows.Remove(Hwnd);
RememberDestroyedWindow(Window);
PresentDesktop();
return true;
}
private void RememberDestroyedWindow(WinWindow Window)
{
if (Window.WndProc == 0)
return;
while (DestroyedWindowOrder.Count >= MaxDestroyedWindows)
DestroyedWindows.Remove(DestroyedWindowOrder.Dequeue());
DestroyedWindows[Window.Hwnd] = Window;
DestroyedWindowOrder.Enqueue(Window.Hwnd);
}
public WinWindow GetDestroyedWindow(ulong Hwnd)
{
if (Hwnd == 0)
return null;
return DestroyedWindows.TryGetValue(Hwnd, out WinWindow Window) ? Window : null;
}
public void ForgetDestroyedWindow(ulong Hwnd)
{
DestroyedWindows.Remove(Hwnd);
}
public WinHandle OpenFileHandle(string Path, bool FSAccess, AccessMask Permissions)
{
WinFile hFile = null;