Files
better-genshin-impact/Fischless.GameCapture/BitBlt/BitBltSession.cs
Shatyuka 66afbc83ae 截图优化 (#1573)
* 截图优化

* 窗口选择排除WS_EX_LAYERED

云原神

* 优化原神窗口判断
2025-05-12 22:39:02 +08:00

239 lines
7.1 KiB
C#

using System.Collections.Concurrent;
using System.Diagnostics;
using System.Runtime.InteropServices;
using OpenCvSharp;
using Vanara.PInvoke;
namespace Fischless.GameCapture.BitBlt;
public class BitBltSession : IDisposable
{
// 窗口句柄
private readonly HWND _hWnd;
private readonly object _lockObject = new();
// 位图句柄
private Gdi32.SafeHBITMAP _hBitmap;
// 位图数据指针,这个指针会在位图释放时自动释放
private IntPtr _bitsPtr;
// 位图数据一行字节数
private readonly int _stride;
// 缓冲区 CompatibleDC
private Gdi32.SafeHDC _hdcDest;
// 来源DC
private User32.SafeReleaseHDC _hdcSrc;
// 旧位图,析构时一起释放掉
private HGDIOBJ _oldBitmap;
// Bitmap buffer 大小
private readonly int _bufferSize;
// Bitmap 内存池
private readonly ConcurrentStack<IntPtr> _bufferPool = [];
// 窗口原宽高
public int Width { get; }
public int Height { get; }
/// <summary>
/// 不是所有的失效情况都能被检测到
/// </summary>
/// <returns></returns>
public bool Invalid => _hWnd.IsNull || _hdcSrc.IsInvalid || _hdcDest.IsInvalid || _hBitmap.IsInvalid || _bitsPtr == 0;
public BitBltSession(HWND hWnd, int w, int h)
{
if (hWnd.IsNull) throw new Exception("hWnd is invalid");
_hWnd = hWnd;
if (w <= 0 || h <= 0) throw new Exception("Invalid width or height");
// 这俩仅做标记
Width = w;
Height = h;
lock (_lockObject)
{
try
{
_hdcSrc = User32.GetDC(_hWnd);
if (_hdcSrc.IsInvalid) throw new Exception($"Failed to get DC for {_hWnd}");
var hdcRasterCaps = Gdi32.GetDeviceCaps(_hdcSrc, Gdi32.DeviceCap.RASTERCAPS);
if ((hdcRasterCaps | 1) == 0) // RC_BITBLT
// 设备不支持 BitBlt
throw new Exception("BitBlt not supported");
var hdcSrcPixel = Gdi32.GetDeviceCaps(_hdcSrc, Gdi32.DeviceCap.BITSPIXEL);
// 颜色位数
if (hdcSrcPixel != 32 && hdcSrcPixel != 24)
// 目前只考虑支持24/32位像素格式
throw new Exception("BitBlt only support 24 or 32 bit pixel color");
var hdcSrcPlanes = Gdi32.GetDeviceCaps(_hdcSrc, Gdi32.DeviceCap.PLANES);
// 颜色平面数
if (hdcSrcPlanes > 1)
// 意味着色深不是标准色深
throw new Exception("BitBlt only support 1 plane");
var hdcClip = Gdi32.GetDeviceCaps(_hdcSrc, Gdi32.DeviceCap.CLIPCAPS);
if (hdcClip == 0)
// 设备不支持剪切出单一窗口
throw new Exception("Device does not support clipping");
_hdcDest = Gdi32.CreateCompatibleDC(_hdcSrc);
if (_hdcDest.IsInvalid)
{
Debug.Fail("Failed to create CompatibleDC");
throw new Exception($"Failed to create CompatibleDC for {_hWnd}");
}
var bmi = new Gdi32.BITMAPINFO
{
bmiHeader = new Gdi32.BITMAPINFOHEADER
{
biSize = (uint)Marshal.SizeOf<Gdi32.BITMAPINFOHEADER>(),
biWidth = Width,
biHeight = -Height, // Top-down image
biPlanes = 1,
biBitCount = 24,
biCompression = Gdi32.BitmapCompressionMode.BI_RGB, // 内存里是BGR
biSizeImage = 0
}
};
_hBitmap = Gdi32.CreateDIBSection(_hdcDest, bmi, Gdi32.DIBColorMode.DIB_RGB_COLORS, out _bitsPtr,
IntPtr.Zero);
if (_hBitmap.IsInvalid || _bitsPtr == 0)
{
if (!_hBitmap.IsInvalid) Gdi32.DeleteObject(_hBitmap);
throw new Exception($"Failed to create dIB section for {_hdcDest}");
}
var bitmap = Gdi32.GetObject<Gdi32.BITMAP>(_hBitmap);
if (bitmap.bmPlanes != 1 || bitmap.bmBitsPixel != 24)
throw new Exception("Unsupported bitmap format");
_stride = bitmap.bmWidthBytes;
_bufferSize = bitmap.bmWidth * bitmap.bmHeight * 3;
_oldBitmap = Gdi32.SelectObject(_hdcDest, _hBitmap);
if (_oldBitmap.IsNull) throw new Exception("Failed to select object");
}
catch (Exception)
{
ReleaseResources();
throw;
}
finally
{
Gdi32.GdiFlush();
}
}
}
public void Dispose()
{
lock (_lockObject)
{
ReleaseResources();
}
GC.SuppressFinalize(this);
}
/// <summary>
/// 调用GDI复制到缓冲区并返回新Mat
/// </summary>
public unsafe Mat? GetImage()
{
lock (_lockObject)
{
// 截图
var success = Gdi32.BitBlt(_hdcDest, 0, 0, Width, Height,
_hdcSrc, 0, 0, Gdi32.RasterOperationMode.SRCCOPY);
if (!success || !Gdi32.GdiFlush()) return null;
// 新Mat
var buffer = AcquireBuffer();
var step = Width * 3;
if (_stride == step)
{
Buffer.MemoryCopy(_bitsPtr.ToPointer(), buffer.ToPointer(), _bufferSize, _bufferSize);
}
else
{
for (var i = 0; i < Height; i++)
{
Buffer.MemoryCopy((void*)(_bitsPtr + _stride * i), (void*)(buffer + step * i), step, step);
}
}
return BitBltMat.FromPixelData(this, Height, Width, MatType.CV_8UC3, buffer, step);
}
}
private IntPtr AcquireBuffer()
{
if (!_bufferPool.TryPop(out var buffer))
{
buffer = Marshal.AllocHGlobal(_bufferSize);
}
return buffer;
}
public void ReleaseBuffer(IntPtr buffer)
{
_bufferPool.Push(buffer);
}
/// <summary>
/// 需加锁环境下访问,释放资源
/// </summary>
private void ReleaseResources()
{
Gdi32.GdiFlush();
if (!_oldBitmap.IsNull)
{
// 先选回资源再释放_hBitmap
Gdi32.SelectObject(_hdcDest, _oldBitmap);
_oldBitmap = Gdi32.SafeHBITMAP.Null;
}
if (!_hBitmap.IsNull)
{
Gdi32.DeleteObject(_hBitmap);
_hBitmap = Gdi32.SafeHBITMAP.Null;
}
if (!_hdcDest.IsNull)
{
Gdi32.DeleteDC(_hdcDest);
_hdcDest = Gdi32.SafeHDC.Null;
}
if (_hdcSrc != IntPtr.Zero)
{
User32.ReleaseDC(_hWnd, _hdcSrc);
_hdcSrc = User32.SafeReleaseHDC.Null;
}
_bitsPtr = IntPtr.Zero;
foreach (var buffer in _bufferPool)
{
Marshal.FreeHGlobal(buffer);
}
}
}