fix(mirror): release GDI handle leaks, add monochrome cursor

This commit is contained in:
Nguyen Duy
2026-05-14 16:50:59 +07:00
parent 7e98eb6638
commit 50d1bf1c6c
+45 -14
View File
@@ -24,12 +24,12 @@ namespace ParsecVDisplay
Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
}
protected override void OnClosing(CancelEventArgs e)
protected override void OnFormClosing(FormClosingEventArgs e)
{
IsMirroring = false;
MirrorThread?.Join();
base.OnClosing(e);
base.OnFormClosing(e);
}
protected override void OnPaint(PaintEventArgs e)
@@ -185,16 +185,20 @@ namespace ParsecVDisplay
private static void DrawScreen(IntPtr dc, IntPtr dcSrc, ref Viewport vp, ref Rectangle screen)
{
// set scaling mode
// HALFTONE gives the cleanest scaled output; MSDN requires calling
// SetBrushOrgEx immediately after to avoid a faint color/dither
// shift in the result.
Native.SetStretchBltMode(dc, /*HALFTONE*/ 4);
Native.SetBrushOrgEx(dc, 0, 0, IntPtr.Zero);
// draw the screen
// SRCCOPY | CAPTUREBLT — CAPTUREBLT pulls in layered / DWM-composited
// windows (Discord, Teams, some game overlays) that plain SRCCOPY skips.
Native.StretchBlt(
dc,
vp.X, vp.Y, vp.Width, vp.Height,
dcSrc,
screen.X, screen.Y, screen.Width, screen.Height,
Native.SRCCOPY
Native.SRCCOPY | Native.CAPTUREBLT
);
}
@@ -203,17 +207,31 @@ namespace ParsecVDisplay
var cursor = default(Native.CURSORINFO);
cursor.cbSize = Marshal.SizeOf<Native.CURSORINFO>();
if (Native.GetCursorInfo(ref cursor)
// cursor must be inside the screen
&& screen.Contains(cursor.screenPosX, cursor.screenPosY)
// and visible
&& cursor.flags == /*CURSOR_SHOWING*/ 0x1)
{
var iconInfo = default(Native.ICONINFO);
Native.GetIconInfo(cursor.hCursor, ref iconInfo);
if (!Native.GetCursorInfo(ref cursor)
|| !screen.Contains(cursor.screenPosX, cursor.screenPosY)
|| cursor.flags != /*CURSOR_SHOWING*/ 0x1)
return;
var iconInfo = default(Native.ICONINFO);
if (!Native.GetIconInfo(cursor.hCursor, ref iconInfo))
return;
try
{
var bmpCursor = default(Native.BITMAP);
// Monochrome (mask-only) cursors — e.g. the text I-beam — have a
// NULL hbmColor. Their geometry lives in hbmMask, whose height is
// 2× the displayed height (top half = AND mask, bottom = XOR mask).
if (iconInfo.hbmColor != IntPtr.Zero)
{
Native.GetObject(iconInfo.hbmColor, Marshal.SizeOf<Native.BITMAP>(), ref bmpCursor);
}
else
{
Native.GetObject(iconInfo.hbmMask, Marshal.SizeOf<Native.BITMAP>(), ref bmpCursor);
bmpCursor.bmHeight /= 2;
}
int x = cursor.screenPosX - iconInfo.xHotspot - screen.X;
int y = cursor.screenPosY - iconInfo.yHotspot - screen.Y;
@@ -223,6 +241,14 @@ namespace ParsecVDisplay
Native.DrawIconEx(dc, x, y, cursor.hCursor, width, height, 0, IntPtr.Zero, /*DI_NORMAL*/ 0x3);
}
finally
{
// GetIconInfo creates these bitmaps and the caller MUST delete
// them — leaking them here (60×/sec at 30 fps) eventually
// exhausts the process's GDI handle quota.
if (iconInfo.hbmMask != IntPtr.Zero) Native.DeleteObject(iconInfo.hbmMask);
if (iconInfo.hbmColor != IntPtr.Zero) Native.DeleteObject(iconInfo.hbmColor);
}
}
private static Size GetClientSize(IntPtr hwnd)
@@ -282,7 +308,8 @@ namespace ParsecVDisplay
private static class Native
{
public const int ENUM_CURRENT_SETTINGS = -1;
public const int SRCCOPY = 0x00CC0020;
public const uint SRCCOPY = 0x00CC0020;
public const uint CAPTUREBLT = 0x40000000;
[StructLayout(LayoutKind.Sequential)]
public struct DEVMODE
@@ -347,6 +374,10 @@ namespace ParsecVDisplay
[DllImport("gdi32.dll")]
public static extern int SetStretchBltMode(IntPtr hdc, int mode);
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool SetBrushOrgEx(IntPtr hdc, int x, int y, IntPtr lppt);
[DllImport("gdi32.dll")]
public static extern bool StretchBlt(IntPtr hdcDest, int nXOriginDest, int nYOriginDest, int nWidthDest, int nHeightDest,
IntPtr hdcSrc, int nXOriginSrc, int nYOriginSrc, int nWidthSrc, int nHeightSrc, uint dwRop);