diff --git a/src/Techsola.InstantReplay/AnimatedCursorRenderer.cs b/src/Techsola.InstantReplay/AnimatedCursorRenderer.cs index 239f9a7..23a7ae6 100644 --- a/src/Techsola.InstantReplay/AnimatedCursorRenderer.cs +++ b/src/Techsola.InstantReplay/AnimatedCursorRenderer.cs @@ -42,28 +42,37 @@ public void Render(DeleteDCSafeHandle deviceContext, HCURSOR cursorHandle, int c if (!cursorAnimationStepByHandle.TryGetValue(cursorHandle, out var cursorAnimationStep)) cursorAnimationStep = (Current: 0, Max: uint.MaxValue); - while (!PInvoke.DrawIconEx( - deviceContext, - cursorX - (int)cursorInfo.Hotspot.X, - cursorY - (int)cursorInfo.Hotspot.Y, - /* Workaround for https://github.com/microsoft/CsWin32/issues/256 - ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ */ - new UnownedHandle(cursorHandle), - cxWidth: 0, - cyWidth: 0, - cursorAnimationStep.Current, - hbrFlickerFreeDraw: null, - DI_FLAGS.DI_NORMAL)) + var deviceContextNeedsRelease = false; + deviceContext.DangerousAddRef(ref deviceContextNeedsRelease); + try { - var lastError = Marshal.GetLastWin32Error(); - - if ((ERROR)lastError == ERROR.INVALID_PARAMETER && cursorAnimationStep.Current > 0) + while (!PInvoke.DrawIconEx( + (HDC)deviceContext.DangerousGetHandle(), + cursorX - (int)cursorInfo.Hotspot.X, + cursorY - (int)cursorInfo.Hotspot.Y, + /* Workaround for https://github.com/microsoft/CsWin32/issues/256 + ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ */ + new UnownedHandle(cursorHandle), + cxWidth: 0, + cyWidth: 0, + cursorAnimationStep.Current, + hbrFlickerFreeDraw: null, + DI_FLAGS.DI_NORMAL)) { - cursorAnimationStep = (Current: 0, Max: cursorAnimationStep.Current - 1); - continue; - } + var lastError = Marshal.GetLastWin32Error(); + + if ((ERROR)lastError == ERROR.INVALID_PARAMETER && cursorAnimationStep.Current > 0) + { + cursorAnimationStep = (Current: 0, Max: cursorAnimationStep.Current - 1); + continue; + } - throw new Win32Exception(lastError); + throw new Win32Exception(lastError); + } + } + finally + { + if (deviceContextNeedsRelease) deviceContext.DangerousRelease(); } cursorAnimationStep.Current = cursorAnimationStep.Current == cursorAnimationStep.Max ? 0 : cursorAnimationStep.Current + 1; diff --git a/src/Techsola.InstantReplay/Composition.cs b/src/Techsola.InstantReplay/Composition.cs index cdde78f..25ff0f3 100644 --- a/src/Techsola.InstantReplay/Composition.cs +++ b/src/Techsola.InstantReplay/Composition.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using System.Runtime.InteropServices; +using Techsola.InstantReplay.Native; using Windows.Win32; using Windows.Win32.Graphics.Gdi; @@ -34,27 +35,37 @@ public Composition(uint width, uint height, ushort bitsPerPixel) BytesPerPixel = (byte)(bitsPerPixel >> 3); Stride = (((width * BytesPerPixel) + 3) / 4) * 4; - DeviceContext = PInvoke.CreateCompatibleDC(null).ThrowWithoutLastErrorAvailableIfInvalid(nameof(PInvoke.CreateCompatibleDC)); - unsafe + DeviceContext = new DeleteDCSafeHandle(PInvoke.CreateCompatibleDC(default)).ThrowWithoutLastErrorAvailableIfInvalid(nameof(PInvoke.CreateCompatibleDC)); + var deviceContextNeedsRelease = false; + DeviceContext.DangerousAddRef(ref deviceContextNeedsRelease); + try { - bitmap = PInvoke.CreateDIBSection(DeviceContext, new() + unsafe { - bmiHeader = + var bitmapInfo = new BITMAPINFO { - biSize = (uint)Marshal.SizeOf(typeof(BITMAPINFOHEADER)), - biWidth = (int)width, - biHeight = -(int)height, - biPlanes = 1, - biBitCount = bitsPerPixel, - }, - }, DIB_USAGE.DIB_RGB_COLORS, out var pointer, hSection: null, offset: 0).ThrowLastErrorIfInvalid(); + bmiHeader = + { + biSize = (uint)Marshal.SizeOf(typeof(BITMAPINFOHEADER)), + biWidth = (int)width, + biHeight = -(int)height, + biPlanes = 1, + biBitCount = bitsPerPixel, + }, + }; - PixelDataPointer = (byte*)pointer; - } + bitmap = PInvoke.CreateDIBSection((HDC)DeviceContext.DangerousGetHandle(), &bitmapInfo, DIB_USAGE.DIB_RGB_COLORS, out var pointer, hSection: null, offset: 0).ThrowLastErrorIfInvalid(); + + PixelDataPointer = (byte*)pointer; + } - // Workaround for https://github.com/microsoft/CsWin32/issues/199 - if (PInvoke.SelectObject(DeviceContext, (HGDIOBJ)bitmap.DangerousGetHandle()).IsNull) - throw new Win32Exception("SelectObject failed."); + if (PInvoke.SelectObject((HDC)DeviceContext.DangerousGetHandle(), (HGDIOBJ)bitmap.DangerousGetHandle()).IsNull) + throw new Win32Exception("SelectObject failed."); + } + finally + { + if (deviceContextNeedsRelease) DeviceContext.DangerousRelease(); + } } public void Dispose() @@ -67,16 +78,26 @@ public void Clear(int x, int y, int width, int height, ref bool needsGdiFlush) { if (width <= 0 || height <= 0) return; - if (!PInvoke.BitBlt(DeviceContext, x, y, width, height, null, 0, 0, ROP_CODE.BLACKNESS)) + var deviceContextNeedsRelease = false; + DeviceContext.DangerousAddRef(ref deviceContextNeedsRelease); + try { - var lastError = Marshal.GetLastWin32Error(); - if (lastError != 0) throw new Win32Exception(lastError); - needsGdiFlush = true; + if (!PInvoke.BitBlt((HDC)DeviceContext.DangerousGetHandle(), x, y, width, height, hdcSrc: default, 0, 0, ROP_CODE.BLACKNESS)) + { + var lastError = Marshal.GetLastWin32Error(); + if (lastError != 0) throw new Win32Exception(lastError); + needsGdiFlush = true; + } + else + { + needsGdiFlush = false; + } } - else + finally { - needsGdiFlush = false; + if (deviceContextNeedsRelease) DeviceContext.DangerousRelease(); } } } } + diff --git a/src/Techsola.InstantReplay/Frame.cs b/src/Techsola.InstantReplay/Frame.cs index a42d99b..87653e0 100644 --- a/src/Techsola.InstantReplay/Frame.cs +++ b/src/Techsola.InstantReplay/Frame.cs @@ -52,29 +52,40 @@ public void Overwrite( bitmap.Dispose(); } - unsafe + var bitmapDCNeedsRelease = false; + bitmapDC.DangerousAddRef(ref bitmapDCNeedsRelease); + try { - bitmap = PInvoke.CreateDIBSection(bitmapDC, new() + unsafe { - bmiHeader = + var bitmapInfo = new BITMAPINFO { - biSize = (uint)Marshal.SizeOf(typeof(BITMAPINFOHEADER)), - biWidth = bitmapWidth, - biHeight = -bitmapHeight, - biPlanes = 1, - biBitCount = BitsPerPixel, - }, - }, DIB_USAGE.DIB_RGB_COLORS, ppvBits: out _, hSection: null, offset: 0).ThrowLastErrorIfInvalid(); + bmiHeader = + { + biSize = (uint)Marshal.SizeOf(typeof(BITMAPINFOHEADER)), + biWidth = bitmapWidth, + biHeight = -bitmapHeight, + biPlanes = 1, + biBitCount = BitsPerPixel, + }, + }; + + bitmap = PInvoke.CreateDIBSection((HDC)bitmapDC.DangerousGetHandle(), &bitmapInfo, DIB_USAGE.DIB_RGB_COLORS, ppvBits: out _, hSection: null, offset: 0).ThrowLastErrorIfInvalid(); + } + } + finally + { + if (bitmapDCNeedsRelease) bitmapDC.DangerousRelease(); } } // Workaround for https://github.com/microsoft/CsWin32/issues/199 - if (PInvoke.SelectObject(bitmapDC, (HGDIOBJ)bitmap.DangerousGetHandle()).IsNull) + if (PInvoke.SelectObject((HDC)bitmapDC.DangerousGetHandle(), (HGDIOBJ)bitmap.DangerousGetHandle()).IsNull) throw new Win32Exception("SelectObject failed."); retryBitBlt: PInvoke.SetLastError(0); // BitBlt doesn't set the last error if it returns false to indicate that the operation has been batched - if (!PInvoke.BitBlt(bitmapDC, 0, 0, windowMetrics.ClientWidth, windowMetrics.ClientHeight, windowDC, 0, 0, ROP_CODE.SRCCOPY)) + if (!PInvoke.BitBlt((HDC)bitmapDC.DangerousGetHandle(), 0, 0, windowMetrics.ClientWidth, windowMetrics.ClientHeight, (HDC)windowDC.DangerousGetHandle(), 0, 0, ROP_CODE.SRCCOPY)) { var lastError = Marshal.GetLastWin32Error(); if ((ERROR)lastError is ERROR.INVALID_WINDOW_HANDLE or ERROR.DC_NOT_FOUND) @@ -125,7 +136,7 @@ public void Compose( } // Workaround for https://github.com/microsoft/CsWin32/issues/199 - if (PInvoke.SelectObject(bitmapDC, (HGDIOBJ)bitmap.DangerousGetHandle()).IsNull) + if (PInvoke.SelectObject((HDC)bitmapDC.DangerousGetHandle(), (HGDIOBJ)bitmap.DangerousGetHandle()).IsNull) throw new Win32Exception("SelectObject failed."); changedArea = new( @@ -136,12 +147,12 @@ public void Compose( PInvoke.SetLastError(0); // BitBlt doesn't set the last error if it returns false to indicate that the operation has been batched if (!PInvoke.BitBlt( - compositionDC, + (HDC)compositionDC.DangerousGetHandle(), changedArea.Left, changedArea.Top, changedArea.Width, changedArea.Height, - bitmapDC, + (HDC)bitmapDC.DangerousGetHandle(), 0, 0, ROP_CODE.SRCCOPY)) diff --git a/src/Techsola.InstantReplay/InstantReplayCamera.CompositionRenderer.cs b/src/Techsola.InstantReplay/InstantReplayCamera.CompositionRenderer.cs index 4c55c9e..105916e 100644 --- a/src/Techsola.InstantReplay/InstantReplayCamera.CompositionRenderer.cs +++ b/src/Techsola.InstantReplay/InstantReplayCamera.CompositionRenderer.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Windows.Win32; +using Techsola.InstantReplay.Native; using Windows.Win32.UI.WindowsAndMessaging; namespace Techsola.InstantReplay diff --git a/src/Techsola.InstantReplay/InstantReplayCamera.WindowInfo.cs b/src/Techsola.InstantReplay/InstantReplayCamera.WindowInfo.cs index 51410df..ae89882 100644 --- a/src/Techsola.InstantReplay/InstantReplayCamera.WindowInfo.cs +++ b/src/Techsola.InstantReplay/InstantReplayCamera.WindowInfo.cs @@ -1,6 +1,5 @@ using System; using Techsola.InstantReplay.Native; -using Windows.Win32; namespace Techsola.InstantReplay { diff --git a/src/Techsola.InstantReplay/InstantReplayCamera.cs b/src/Techsola.InstantReplay/InstantReplayCamera.cs index b30ff8a..6fe650e 100644 --- a/src/Techsola.InstantReplay/InstantReplayCamera.cs +++ b/src/Techsola.InstantReplay/InstantReplayCamera.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Drawing; using System.Linq; using System.Runtime.InteropServices; using System.Threading; @@ -115,14 +116,14 @@ private static void AddFrames(object? state) var currentWindows = (windowEnumerator ??= new()).GetCurrentWindowHandlesInZOrder(); - bitmapDC ??= PInvoke.CreateCompatibleDC(null).ThrowWithoutLastErrorAvailableIfInvalid(nameof(PInvoke.CreateCompatibleDC)); + bitmapDC ??= new DeleteDCSafeHandle(PInvoke.CreateCompatibleDC(default)).ThrowWithoutLastErrorAvailableIfInvalid(nameof(PInvoke.CreateCompatibleDC)); lock (InfoByWindowHandle) { Frames.Add(( Timestamp: now, Cursor: (cursorInfo.flags & (CURSORINFO_FLAGS.CURSOR_SHOWING | CURSORINFO_FLAGS.CURSOR_SUPPRESSED)) == CURSORINFO_FLAGS.CURSOR_SHOWING - ? (cursorInfo.ptScreenPos.x, cursorInfo.ptScreenPos.y, cursorInfo.hCursor) + ? (cursorInfo.ptScreenPos.X, cursorInfo.ptScreenPos.Y, cursorInfo.hCursor) : null)); var zOrder = 0u; @@ -215,7 +216,7 @@ private static void AddFrames(object? state) private static WindowMetrics? GetWindowMetricsIfExists(HWND window) { - var clientTopLeft = default(POINT); + var clientTopLeft = default(Point); if (!PInvoke.ClientToScreen(window, ref clientTopLeft)) return null; // This is what happens when the window handle becomes invalid. @@ -226,7 +227,7 @@ private static void AddFrames(object? state) throw new Win32Exception(lastError); } - return new(clientTopLeft.x, clientTopLeft.y, clientRect.right, clientRect.bottom); + return new(clientTopLeft.X, clientTopLeft.Y, clientRect.right, clientRect.bottom); } #if !NET35 diff --git a/src/Techsola.InstantReplay/Native/DeleteDCSafeHandle.cs b/src/Techsola.InstantReplay/Native/DeleteDCSafeHandle.cs new file mode 100644 index 0000000..22a0de1 --- /dev/null +++ b/src/Techsola.InstantReplay/Native/DeleteDCSafeHandle.cs @@ -0,0 +1,22 @@ +using System; +using System.Runtime.InteropServices; +using Windows.Win32; +using Windows.Win32.Graphics.Gdi; + +namespace Techsola.InstantReplay.Native; + +// Workaround for https://github.com/microsoft/CsWin32/issues/209 +internal sealed class DeleteDCSafeHandle : SafeHandle +{ + public DeleteDCSafeHandle(IntPtr handle) : base(invalidHandleValue: IntPtr.Zero, ownsHandle: true) + { + SetHandle(handle); + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected override bool ReleaseHandle() + { + return (bool)PInvoke.DeleteDC((HDC)handle); + } +} diff --git a/src/Techsola.InstantReplay/Native/WindowDeviceContextSafeHandle.cs b/src/Techsola.InstantReplay/Native/WindowDeviceContextSafeHandle.cs index 7c97c41..94af73d 100644 --- a/src/Techsola.InstantReplay/Native/WindowDeviceContextSafeHandle.cs +++ b/src/Techsola.InstantReplay/Native/WindowDeviceContextSafeHandle.cs @@ -1,29 +1,27 @@ using System; using System.Runtime.InteropServices; -using System.Runtime.Versioning; using Windows.Win32; using Windows.Win32.Foundation; +using Windows.Win32.Graphics.Gdi; -namespace Techsola.InstantReplay.Native +namespace Techsola.InstantReplay.Native; + +// Workaround for https://github.com/microsoft/CsWin32/issues/209 +internal sealed class WindowDeviceContextSafeHandle : SafeHandle { - // Workaround for https://github.com/microsoft/CsWin32/issues/209 - internal sealed class WindowDeviceContextSafeHandle : DeleteDCSafeHandle + public WindowDeviceContextSafeHandle(HWND hWnd, IntPtr handle) + : base(invalidHandleValue: IntPtr.Zero, ownsHandle: true) { - public WindowDeviceContextSafeHandle(HWND hWnd, IntPtr handle) - : base(handle) - { - HWnd = hWnd; - } + HWnd = hWnd; + SetHandle(handle); + } - public HWND HWnd { get; } + public HWND HWnd { get; } - protected override bool ReleaseHandle() => ReleaseDC(HWnd, handle); + public override bool IsInvalid => handle == IntPtr.Zero; - /// - /// - /// - [SupportedOSPlatform("windows")] - [DllImport("user32.dll")] - private static extern bool ReleaseDC(HWND hWnd, IntPtr hDC); + protected override bool ReleaseHandle() + { + return PInvoke.ReleaseDC(HWnd, (HDC)handle) == 1; } } diff --git a/src/Techsola.InstantReplay/NativeMethods.txt b/src/Techsola.InstantReplay/NativeMethods.txt index b9ac0fb..83f261b 100644 --- a/src/Techsola.InstantReplay/NativeMethods.txt +++ b/src/Techsola.InstantReplay/NativeMethods.txt @@ -3,6 +3,7 @@ BITMAP ClientToScreen CreateCompatibleDC CreateDIBSection +DeleteDC DrawIconEx EnumWindows GdiFlush @@ -13,5 +14,6 @@ GetIconInfo GetObject GetWindowThreadProcessId IsWindowVisible +ReleaseDC SelectObject SetLastError diff --git a/src/Techsola.InstantReplay/Techsola.InstantReplay.csproj b/src/Techsola.InstantReplay/Techsola.InstantReplay.csproj index 31094dc..ef4062b 100644 --- a/src/Techsola.InstantReplay/Techsola.InstantReplay.csproj +++ b/src/Techsola.InstantReplay/Techsola.InstantReplay.csproj @@ -19,11 +19,14 @@ true true snupkg + + + $(NoWarn);PInvoke009 - +