mirror of
https://github.com/babalae/better-genshin-impact.git
synced 2026-03-15 07:43:20 +08:00
截图优化 (#1480)
* BitBlt 优化 * BitBlt恢复Top-down * 渲染时翻转图像 * CaptureSession引用计数 * 恢复成无拷贝Mat * 合法性检查 * 优化截图预览窗口 * 保存截图文件必要时需要克隆一份Mat * BitBlt内存池 * 返回拷贝就不用对Session做引用计数了 * 移除CaptureImageRes * 优化DirectX * 更好地处理padding * BitBlt去掉padding 1920*1080的游戏窗口是4字节对齐的,因此不会有性能影响。这里主要用于测试。 * 修复修改窗口大小 * 合并CreateStagingTexture * 修复设备丢失崩溃 * WGC截图支持HDR * fix typo * CodeQA * 去掉1px窗口边框 * DirectX截图去掉A通道 * HDR转换使用GPU加速 --------- Co-authored-by: 辉鸭蛋 <huiyadanli@gmail.com>
This commit is contained in:
@@ -15,9 +15,13 @@ public class BitBltCapture : IGameCapture
|
||||
|
||||
private volatile bool _lastCaptureFailed;
|
||||
|
||||
public void Dispose() => Stop();
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public CaptureImageRes? Capture() => Capture(false);
|
||||
public Mat? Capture() => Capture(false);
|
||||
|
||||
public void Start(nint hWnd, Dictionary<string, object>? settings = null)
|
||||
{
|
||||
@@ -50,9 +54,8 @@ public class BitBltCapture : IGameCapture
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 检查窗口大小,如果改变则更新截图尺寸。返回是否成功。
|
||||
/// 检查窗口大小,如果改变则更新截图尺寸。
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private void CheckSession()
|
||||
{
|
||||
if (_lockSlim.WaitingWriteCount > 0 || !_lockSlim.TryEnterWriteLock(TimeSpan.FromSeconds(0.5)))
|
||||
@@ -66,7 +69,7 @@ public class BitBltCapture : IGameCapture
|
||||
{
|
||||
// 窗口状态变化可能会导致会话失效
|
||||
// 上次截图失败则重置会话,避免一直截图失败
|
||||
if (_session is not null && (_session.IsInvalid() || _lastCaptureFailed))
|
||||
if (_session is not null && (_session.Invalid || _lastCaptureFailed))
|
||||
{
|
||||
_session.Dispose();
|
||||
_session = null;
|
||||
@@ -84,15 +87,18 @@ public class BitBltCapture : IGameCapture
|
||||
var width = windowRect.right - windowRect.left;
|
||||
var height = windowRect.bottom - windowRect.top;
|
||||
|
||||
_session ??= new BitBltSession(_hWnd, width, height);
|
||||
if (_session.Width == width && _session.Height == height)
|
||||
if (_session != null)
|
||||
{
|
||||
// 窗口大小没有改变
|
||||
return;
|
||||
if (_session.Width == width && _session.Height == height)
|
||||
{
|
||||
// 窗口大小没有改变
|
||||
return;
|
||||
}
|
||||
|
||||
// 窗口尺寸被改变,释放资源,然后重新创建会话
|
||||
_session.Dispose();
|
||||
}
|
||||
|
||||
// 窗口尺寸被改变,释放资源,然后重新创建会话
|
||||
_session.Dispose();
|
||||
_session = new BitBltSession(_hWnd, width, height);
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -110,7 +116,7 @@ public class BitBltCapture : IGameCapture
|
||||
/// </summary>
|
||||
/// <param name="recursive">递归标志</param>
|
||||
/// <returns>截图</returns>
|
||||
private CaptureImageRes? Capture(bool recursive)
|
||||
private Mat? Capture(bool recursive)
|
||||
{
|
||||
if (_hWnd == IntPtr.Zero)
|
||||
{
|
||||
@@ -139,13 +145,13 @@ public class BitBltCapture : IGameCapture
|
||||
{
|
||||
// 成功截图
|
||||
_lastCaptureFailed = false;
|
||||
return CaptureImageRes.BuildNullable(result);
|
||||
return result;
|
||||
}
|
||||
else if (result is null)
|
||||
else
|
||||
{
|
||||
if (_lastCaptureFailed) return CaptureImageRes.BuildNullable(result); // 这不是首次失败,不再进行尝试
|
||||
if (_lastCaptureFailed) return result; // 这不是首次失败,不再进行尝试
|
||||
_lastCaptureFailed = true; // 设置失败标志
|
||||
if (recursive) return CaptureImageRes.BuildNullable(result); // 已设置递归标志,说明也不是首次失败
|
||||
if (recursive) return result; // 已设置递归标志,说明也不是首次失败
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -165,24 +171,15 @@ public class BitBltCapture : IGameCapture
|
||||
/// 截图功能的实现。需要加锁后调用,一般只由 Capture 方法调用。
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private Bitmap? Capture0()
|
||||
private Mat? Capture0()
|
||||
{
|
||||
Bitmap? bitmap = null;
|
||||
try
|
||||
{
|
||||
if (_session is null)
|
||||
{
|
||||
// 没有成功创建会话,直接返回空
|
||||
return null;
|
||||
}
|
||||
bitmap = _session.GetImage();
|
||||
return bitmap;
|
||||
return _session?.GetImage();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// 理论这里不应出现异常,除非窗口不存在了或者有什么bug
|
||||
// 出现异常的时候释放内存
|
||||
bitmap?.Dispose();
|
||||
Error.WriteLine("[BitBlt]Failed to capture image {0}", e);
|
||||
return null;
|
||||
}
|
||||
@@ -208,4 +205,4 @@ public class BitBltCapture : IGameCapture
|
||||
|
||||
IsCapturing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
Fischless.GameCapture/BitBlt/BitBltMat.cs
Normal file
32
Fischless.GameCapture/BitBlt/BitBltMat.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using OpenCvSharp;
|
||||
using OpenCvSharp.Internal;
|
||||
|
||||
namespace Fischless.GameCapture.BitBlt;
|
||||
|
||||
public class BitBltMat : Mat
|
||||
{
|
||||
private readonly BitBltSession _session;
|
||||
private readonly IntPtr _data;
|
||||
|
||||
private BitBltMat(IntPtr ptr, BitBltSession session, IntPtr data)
|
||||
{
|
||||
if (ptr == IntPtr.Zero)
|
||||
throw new OpenCvSharpException("Native object address is NULL");
|
||||
this.ptr = ptr;
|
||||
_session = session;
|
||||
_data = data;
|
||||
}
|
||||
|
||||
public static Mat FromPixelData(BitBltSession session, int rows, int cols, MatType type, IntPtr data, long step = 0)
|
||||
{
|
||||
NativeMethods.HandleException(
|
||||
NativeMethods.core_Mat_new8(rows, cols, type, data, new IntPtr(step), out var ptr));
|
||||
return new BitBltMat(ptr, session, data);
|
||||
}
|
||||
|
||||
protected override void DisposeUnmanaged()
|
||||
{
|
||||
base.DisposeUnmanaged();
|
||||
_session.ReleaseBuffer(_data);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using Vanara.PInvoke;
|
||||
|
||||
namespace Fischless.GameCapture.BitBlt;
|
||||
|
||||
public class BitBltOldCapture : IGameCapture
|
||||
{
|
||||
private nint _hWnd;
|
||||
|
||||
public static object LockObject { get; } = new();
|
||||
|
||||
public bool IsCapturing { get; private set; }
|
||||
|
||||
public void Dispose() => Stop();
|
||||
|
||||
public void Start(nint hWnd, Dictionary<string, object>? settings = null)
|
||||
{
|
||||
_hWnd = hWnd;
|
||||
IsCapturing = true;
|
||||
if (settings != null && settings.TryGetValue("autoFixWin11BitBlt", out var value))
|
||||
{
|
||||
if (value is true)
|
||||
{
|
||||
BitBltRegistryHelper.SetDirectXUserGlobalSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public CaptureImageRes? Capture()
|
||||
{
|
||||
if (_hWnd == IntPtr.Zero)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
lock (LockObject)
|
||||
{
|
||||
User32.GetClientRect(_hWnd, out var windowRect);
|
||||
int x = default, y = default;
|
||||
var width = windowRect.right - windowRect.left;
|
||||
var height = windowRect.bottom - windowRect.top;
|
||||
|
||||
Bitmap bitmap = new(width, height);
|
||||
using System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bitmap);
|
||||
var hdcDest = g.GetHdc();
|
||||
var hdcSrc = User32.GetDC(_hWnd == IntPtr.Zero ? User32.GetDesktopWindow() : _hWnd);
|
||||
Gdi32.StretchBlt(hdcDest, 0, 0, width, height, hdcSrc, x, y, width, height,
|
||||
Gdi32.RasterOperationMode.SRCCOPY);
|
||||
g.ReleaseHdc();
|
||||
Gdi32.DeleteDC(hdcDest);
|
||||
Gdi32.DeleteDC(hdcSrc);
|
||||
return CaptureImageRes.BuildNullable(bitmap);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.WriteLine(e);
|
||||
}
|
||||
|
||||
return null!;
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_hWnd = IntPtr.Zero;
|
||||
IsCapturing = false;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using OpenCvSharp;
|
||||
@@ -7,17 +8,6 @@ namespace Fischless.GameCapture.BitBlt;
|
||||
|
||||
public class BitBltSession : IDisposable
|
||||
{
|
||||
// 用于过滤alpha通道
|
||||
private static readonly Gdi32.BLENDFUNCTION BlendFunction = new()
|
||||
{
|
||||
BlendOp = 0, //Gdi32.BlendOperation.AC_SRC_OVER,
|
||||
BlendFlags = 0,
|
||||
SourceConstantAlpha = 255,
|
||||
AlphaFormat = 0
|
||||
};
|
||||
|
||||
private readonly int _height;
|
||||
|
||||
// 窗口句柄
|
||||
private readonly HWND _hWnd;
|
||||
|
||||
@@ -25,13 +15,17 @@ public class BitBltSession : IDisposable
|
||||
|
||||
// 大小计算好的宽高,截图用这个不会爆炸
|
||||
private readonly int _width;
|
||||
|
||||
// 位图指针,这个指针会在位图释放时自动释放
|
||||
private IntPtr _bitsPtr;
|
||||
private readonly int _height;
|
||||
|
||||
// 位图句柄
|
||||
private Gdi32.SafeHBITMAP _hBitmap;
|
||||
|
||||
// 位图数据指针,这个指针会在位图释放时自动释放
|
||||
private IntPtr _bitsPtr;
|
||||
|
||||
// 位图数据一行字节数
|
||||
private readonly int _stride;
|
||||
|
||||
// 缓冲区 CompatibleDC
|
||||
private Gdi32.SafeHDC _hdcDest;
|
||||
|
||||
@@ -41,17 +35,24 @@ public class BitBltSession : IDisposable
|
||||
// 旧位图,析构时一起释放掉
|
||||
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)
|
||||
{
|
||||
// 分配全空对象 避免 System.NullReferenceException
|
||||
// 不能直接在上面写,不然zz编译器会报warning,感觉抑制warning也不优雅
|
||||
_oldBitmap = Gdi32.SafeHBITMAP.Null;
|
||||
_hBitmap = Gdi32.SafeHBITMAP.Null;
|
||||
_hdcDest = Gdi32.SafeHDC.Null;
|
||||
_hdcSrc = User32.SafeReleaseHDC.Null;
|
||||
_bitsPtr = IntPtr.Zero;
|
||||
|
||||
if (hWnd.IsNull) throw new Exception("hWnd is invalid");
|
||||
|
||||
_hWnd = hWnd;
|
||||
@@ -67,7 +68,12 @@ public class BitBltSession : IDisposable
|
||||
try
|
||||
{
|
||||
_hdcSrc = User32.GetDC(_hWnd);
|
||||
if (_hdcSrc.IsInvalid) throw new Exception("Failed to get DC for {_hWnd}");
|
||||
if (_hdcSrc.IsInvalid) throw new Exception($"Failed to get DC for {_hWnd}");
|
||||
|
||||
var hdcRasterCaps = Gdi32.GetDeviceCaps(_hdcSrc, Gdi32.DeviceCap.RASTERCAPS);
|
||||
if ((hdcRasterCaps | 0x800) == 0) // RC_STRETCHBLT
|
||||
// 设备不支持 BitBlt
|
||||
throw new Exception("BitBlt not supported");
|
||||
|
||||
var hdcSrcPixel = Gdi32.GetDeviceCaps(_hdcSrc, Gdi32.DeviceCap.BITSPIXEL);
|
||||
// 颜色位数
|
||||
@@ -87,9 +93,14 @@ public class BitBltSession : IDisposable
|
||||
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 maxW = Gdi32.GetDeviceCaps(_hdcDest, Gdi32.DeviceCap.HORZRES);
|
||||
var maxH = Gdi32.GetDeviceCaps(_hdcDest, Gdi32.DeviceCap.VERTRES);
|
||||
|
||||
if (maxW <= 0 || maxH <= 0)
|
||||
// 显示器不见啦
|
||||
throw new Exception("Can not get display size");
|
||||
@@ -98,12 +109,6 @@ public class BitBltSession : IDisposable
|
||||
_width = Math.Min(maxW, Width);
|
||||
_height = Math.Min(maxH, Height);
|
||||
|
||||
if (_hdcSrc.IsInvalid)
|
||||
{
|
||||
Debug.Fail("Failed to create CompatibleDC");
|
||||
throw new Exception("Failed to create CompatibleDC for {_hWnd}");
|
||||
}
|
||||
|
||||
|
||||
var bmi = new Gdi32.BITMAPINFO
|
||||
{
|
||||
@@ -114,21 +119,27 @@ public class BitBltSession : IDisposable
|
||||
biHeight = -_height, // Top-down image
|
||||
biPlanes = (ushort)hdcSrcPlanes,
|
||||
biBitCount = (ushort)(hdcSrcPixel - 8), //RGBA->RGB
|
||||
biCompression = 0, // BI_RGB
|
||||
biCompression = Gdi32.BitmapCompressionMode.BI_RGB,
|
||||
biSizeImage = 0
|
||||
}
|
||||
};
|
||||
_hBitmap = Gdi32.CreateDIBSection(_hdcDest, bmi, Gdi32.DIBColorMode.DIB_RGB_COLORS, out var bits,
|
||||
_hBitmap = Gdi32.CreateDIBSection(_hdcDest, bmi, Gdi32.DIBColorMode.DIB_RGB_COLORS, out _bitsPtr,
|
||||
IntPtr.Zero);
|
||||
|
||||
if (_hBitmap.IsInvalid || bits == 0)
|
||||
if (_hBitmap.IsInvalid || _bitsPtr == 0)
|
||||
{
|
||||
if (!_hBitmap.IsInvalid) Gdi32.DeleteObject(_hBitmap);
|
||||
|
||||
throw new Exception($"Failed to create dIB section for {_hWnd}");
|
||||
throw new Exception($"Failed to create dIB section for {_hdcDest}");
|
||||
}
|
||||
|
||||
_bitsPtr = bits;
|
||||
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");
|
||||
}
|
||||
@@ -144,73 +155,58 @@ public class BitBltSession : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
//原宽高,用来喂上层
|
||||
public int Width { get; }
|
||||
public int Height { get; }
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
ReleaseResources();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 调用GDI复制到缓冲区并返回新Image
|
||||
/// 调用GDI复制到缓冲区并返回新Mat
|
||||
/// </summary>
|
||||
public Bitmap? GetImage()
|
||||
public unsafe Mat? GetImage()
|
||||
{
|
||||
lock (_lockObject)
|
||||
{
|
||||
Gdi32.GdiFlush();
|
||||
if (_hBitmap.IsInvalid)
|
||||
// 位图指针无效
|
||||
return null;
|
||||
|
||||
Gdi32.BITMAP bitmap;
|
||||
try
|
||||
{
|
||||
// 获取位图详情,这应该能抵挡住奇奇怪怪的bug
|
||||
bitmap = Gdi32.GetObject<Gdi32.BITMAP>(_hBitmap);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
// 可能是位图已经被释放了
|
||||
return null;
|
||||
}
|
||||
|
||||
// 这个是bitmap的结构信息,不用手动释放
|
||||
if (bitmap.bmPlanes != 1 || bitmap.bmBitsPixel != 24)
|
||||
// 不支持的位图格式
|
||||
return null;
|
||||
// 直接返回转换后的位图
|
||||
var success = Gdi32.AlphaBlend(_hdcDest, 0, 0, _width, _height, _hdcSrc, 0, 0,
|
||||
_width, _height, BlendFunction);
|
||||
// 截图
|
||||
var success = Gdi32.StretchBlt(_hdcDest, 0, 0, _width, _height,
|
||||
_hdcSrc, 0, 0, _width, _height, Gdi32.RasterOperationMode.SRCCOPY);
|
||||
if (!success || !Gdi32.GdiFlush()) return null;
|
||||
|
||||
// return Mat.FromPixelData(bitmap.bmHeight, bitmap.bmWidth, MatType.CV_8UC3, bitmap.bmBits,bitmap.bmWidthBytes);
|
||||
return _hBitmap.ToBitmap();
|
||||
|
||||
// 原始宏 ((((biWidth * biBitCount) + 31) & ~31) >> 3) => (biWidth * biBitCount + 3) & ~3) (在总位数是8的倍数时,两者等价)
|
||||
// 对齐 https://learn.microsoft.com/zh-cn/windows/win32/api/wingdi/ns-wingdi-bitmapinfoheader
|
||||
|
||||
// 适用于32位(RGBA)
|
||||
// return Gdi32.BitBlt(_hdcDest, 0, 0, Width, Height, _hdcSrc, 0, 0, Gdi32.RasterOperationMode.SRCCOPY ) ? Mat.FromPixelData(Height, Width, MatType.CV_8UC3, _bitsPtr) : 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 不是所有的失效情况都能被检测到
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool IsInvalid()
|
||||
private IntPtr AcquireBuffer()
|
||||
{
|
||||
return _hWnd.IsNull || _hdcSrc.IsInvalid || _hdcDest.IsInvalid || _hBitmap.IsInvalid || _bitsPtr == IntPtr.Zero;
|
||||
if (!_bufferPool.TryPop(out var buffer))
|
||||
{
|
||||
buffer = Marshal.AllocHGlobal(_bufferSize);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public void ReleaseBuffer(IntPtr buffer)
|
||||
{
|
||||
_bufferPool.Push(buffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -218,6 +214,8 @@ public class BitBltSession : IDisposable
|
||||
/// </summary>
|
||||
private void ReleaseResources()
|
||||
{
|
||||
Gdi32.GdiFlush();
|
||||
|
||||
if (!_oldBitmap.IsNull)
|
||||
{
|
||||
// 先选回资源再释放_hBitmap
|
||||
@@ -245,7 +243,11 @@ public class BitBltSession : IDisposable
|
||||
_hdcSrc = User32.SafeReleaseHDC.Null;
|
||||
}
|
||||
|
||||
Gdi32.GdiFlush();
|
||||
_bitsPtr = IntPtr.Zero;
|
||||
|
||||
foreach (var buffer in _bufferPool)
|
||||
{
|
||||
Marshal.FreeHGlobal(buffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
using OpenCvSharp;
|
||||
using OpenCvSharp.Extensions;
|
||||
|
||||
namespace Fischless.GameCapture;
|
||||
|
||||
/// <summary>
|
||||
/// 捕获的图像
|
||||
/// </summary>
|
||||
public class CaptureImageRes : IDisposable
|
||||
{
|
||||
public Bitmap? Bitmap { get; set; }
|
||||
public Mat? Mat { get; set; }
|
||||
|
||||
public int Width => Mat?.Width ?? Bitmap?.Width ?? 0;
|
||||
public int Height => Mat?.Height ?? Bitmap?.Height ?? 0;
|
||||
|
||||
public CaptureImageRes(Mat mat)
|
||||
{
|
||||
Mat = mat;
|
||||
}
|
||||
|
||||
public CaptureImageRes(Bitmap bitmap)
|
||||
{
|
||||
Bitmap = bitmap;
|
||||
}
|
||||
|
||||
public static CaptureImageRes? BuildNullable(Bitmap? bitmap)
|
||||
{
|
||||
if (bitmap == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return new CaptureImageRes(bitmap);
|
||||
}
|
||||
|
||||
public static CaptureImageRes? BuildNullable(Mat? mat)
|
||||
{
|
||||
if (mat == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
return new CaptureImageRes(mat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 非特殊情况不要使用这个方法,会造成额外的性能消耗
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Mat? ForceGetMat()
|
||||
{
|
||||
if (Mat == null)
|
||||
{
|
||||
Mat = Bitmap?.ToMat();
|
||||
}
|
||||
return Mat;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 非特殊情况不要使用这个方法,会造成额外的性能消耗
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public Bitmap? ForceGetBitmap()
|
||||
{
|
||||
if (Bitmap == null)
|
||||
{
|
||||
Bitmap = Mat?.ToBitmap();
|
||||
}
|
||||
return Bitmap;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Bitmap?.Dispose();
|
||||
Mat?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,19 @@ namespace Fischless.GameCapture;
|
||||
|
||||
public enum CaptureModes
|
||||
{
|
||||
// [Description("BitBlt(稳定)")]
|
||||
// BitBlt,
|
||||
|
||||
[Description("BitBlt")]
|
||||
BitBlt,
|
||||
|
||||
[Description("WindowsGraphicsCapture")]
|
||||
WindowsGraphicsCapture,
|
||||
|
||||
[DefaultValue(0)]
|
||||
BitBlt = 0,
|
||||
|
||||
[Description("DwmGetDxSharedSurface")]
|
||||
DwmGetDxSharedSurface
|
||||
[DefaultValue(1)]
|
||||
DwmGetDxSharedSurface = 2,
|
||||
|
||||
[Description("WindowsGraphicsCapture")]
|
||||
[DefaultValue(2)]
|
||||
WindowsGraphicsCapture = 1,
|
||||
|
||||
[Description("WindowsGraphicsCapture(HDR)")]
|
||||
[DefaultValue(3)]
|
||||
WindowsGraphicsCaptureHdr = 3,
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Vanara.PInvoke;
|
||||
|
||||
namespace Fischless.GameCapture.DwmSharedSurface.Helpers;
|
||||
|
||||
internal class NativeMethods
|
||||
{
|
||||
|
||||
public delegate bool DwmGetDxSharedSurfaceDelegate(IntPtr hWnd, out IntPtr phSurface, out long pAdapterLuid, out long pFmtWindow, out long pPresentFlags, out long pWin32KUpdateId);
|
||||
|
||||
public static DwmGetDxSharedSurfaceDelegate DwmGetDxSharedSurface;
|
||||
|
||||
static NativeMethods()
|
||||
{
|
||||
var ptr = Kernel32.GetProcAddress(Kernel32.GetModuleHandle("user32"), "DwmGetDxSharedSurface");
|
||||
DwmGetDxSharedSurface = Marshal.GetDelegateForFunctionPointer<DwmGetDxSharedSurfaceDelegate>(ptr);
|
||||
}
|
||||
}
|
||||
@@ -1,128 +1,143 @@
|
||||
using Fischless.GameCapture.DwmSharedSurface.Helpers;
|
||||
using Fischless.GameCapture.Graphics.Helpers;
|
||||
using Fischless.GameCapture.Graphics.Helpers;
|
||||
using SharpDX.Direct3D11;
|
||||
using SharpDX.DXGI;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using OpenCvSharp;
|
||||
using OpenCvSharp.Extensions;
|
||||
using SharpDX;
|
||||
using Vanara.PInvoke;
|
||||
using Device = SharpDX.Direct3D11.Device;
|
||||
|
||||
namespace Fischless.GameCapture.DwmSharedSurface
|
||||
namespace Fischless.GameCapture.DwmSharedSurface;
|
||||
|
||||
public partial class SharedSurfaceCapture : IGameCapture
|
||||
{
|
||||
public class SharedSurfaceCapture : IGameCapture
|
||||
// 窗口句柄
|
||||
private nint _hWnd;
|
||||
|
||||
private static readonly object LockObject = new();
|
||||
|
||||
// D3D 设备
|
||||
private Device? _d3dDevice;
|
||||
|
||||
// 截图区域
|
||||
private ResourceRegion? _region;
|
||||
|
||||
// 暂存贴图
|
||||
private Texture2D? _stagingTexture;
|
||||
|
||||
// Surface 大小
|
||||
private int _surfaceWidth;
|
||||
private int _surfaceHeight;
|
||||
|
||||
public bool IsCapturing { get; private set; }
|
||||
|
||||
[LibraryImport("user32.dll", EntryPoint = "DwmGetDxSharedSurface", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool DwmGetDxSharedSurface(IntPtr hWnd, out IntPtr phSurface, out long pAdapterLuid, out long pFmtWindow, out long pPresentFlags, out long pWin32KUpdateId);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
private nint _hWnd;
|
||||
private static readonly object LockObject = new object();
|
||||
private Device? _d3dDevice;
|
||||
Stop();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public void Dispose() => Stop();
|
||||
public void Start(nint hWnd, Dictionary<string, object>? settings = null)
|
||||
{
|
||||
_hWnd = hWnd;
|
||||
User32.ShowWindow(hWnd, ShowWindowCommand.SW_RESTORE);
|
||||
_region = GetGameScreenRegion(hWnd);
|
||||
_d3dDevice = new Device(SharpDX.Direct3D.DriverType.Hardware, DeviceCreationFlags.BgraSupport); // Software/Hardware
|
||||
|
||||
public bool IsCapturing { get; private set; }
|
||||
IsCapturing = true;
|
||||
}
|
||||
|
||||
private ResourceRegion? _region;
|
||||
|
||||
|
||||
public void Start(nint hWnd, Dictionary<string, object>? settings = null)
|
||||
/// <summary>
|
||||
/// 从 GetWindowRect 的带窗口阴影面积矩形 截取出 GetClientRect的矩形(游戏区域)
|
||||
/// </summary>
|
||||
/// <param name="hWnd"></param>
|
||||
/// <returns></returns>
|
||||
private ResourceRegion? GetGameScreenRegion(nint hWnd)
|
||||
{
|
||||
var exStyle = User32.GetWindowLong(hWnd, User32.WindowLongFlags.GWL_EXSTYLE);
|
||||
if ((exStyle & (int)User32.WindowStylesEx.WS_EX_TOPMOST) != 0)
|
||||
{
|
||||
_hWnd = hWnd;
|
||||
User32.ShowWindow(hWnd, ShowWindowCommand.SW_RESTORE);
|
||||
User32.SetForegroundWindow(hWnd);
|
||||
_region = GetGameScreenRegion(hWnd);
|
||||
_d3dDevice = new Device(SharpDX.Direct3D.DriverType.Hardware, DeviceCreationFlags.BgraSupport); // Software/Hardware
|
||||
|
||||
//var device = Direct3D11Helper.CreateDevice();
|
||||
//_d3dDevice = Direct3D11Helper.CreateSharpDXDevice(device);
|
||||
|
||||
IsCapturing = true;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 GetWindowRect 的带窗口阴影面积矩形 截取出 GetClientRect的矩形(游戏区域)
|
||||
/// </summary>
|
||||
/// <param name="hWnd"></param>
|
||||
/// <returns></returns>
|
||||
private ResourceRegion? GetGameScreenRegion(nint hWnd)
|
||||
ResourceRegion region = new();
|
||||
User32.GetWindowRect(hWnd, out var windowWithShadowRect);
|
||||
DwmApi.DwmGetWindowAttribute<RECT>(hWnd, DwmApi.DWMWINDOWATTRIBUTE.DWMWA_EXTENDED_FRAME_BOUNDS, out var windowRect);
|
||||
User32.GetClientRect(_hWnd, out var clientRect);
|
||||
|
||||
region.Left = windowRect.Left - windowWithShadowRect.Left;
|
||||
// 标题栏 windowRect.Height - clientRect.Height 上阴影 windowRect.Top - windowWithShadowRect.Top
|
||||
region.Top = windowRect.Height - clientRect.Height + windowRect.Top - windowWithShadowRect.Top;
|
||||
region.Right = region.Left + clientRect.Width;
|
||||
region.Bottom = region.Top + clientRect.Height;
|
||||
region.Front = 0;
|
||||
region.Back = 1;
|
||||
|
||||
return region;
|
||||
}
|
||||
|
||||
public Mat? Capture()
|
||||
{
|
||||
lock (LockObject)
|
||||
{
|
||||
var exStyle = User32.GetWindowLong(hWnd, User32.WindowLongFlags.GWL_EXSTYLE);
|
||||
if ((exStyle & (int)User32.WindowStylesEx.WS_EX_TOPMOST) != 0)
|
||||
if (_d3dDevice == null)
|
||||
{
|
||||
Debug.WriteLine("D3Device is null.");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!DwmGetDxSharedSurface(_hWnd, out var phSurface, out _, out _, out _, out _))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (phSurface == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
ResourceRegion region = new();
|
||||
User32.GetWindowRect(hWnd, out var windowWithShadowRect);
|
||||
DwmApi.DwmGetWindowAttribute<RECT>(hWnd, DwmApi.DWMWINDOWATTRIBUTE.DWMWA_EXTENDED_FRAME_BOUNDS, out var windowRect);
|
||||
User32.GetClientRect(_hWnd, out var clientRect);
|
||||
|
||||
region.Left = windowRect.Left - windowWithShadowRect.Left;
|
||||
// 标题栏 windowRect.Height - clientRect.Height 上阴影 windowRect.Top - windowWithShadowRect.Top
|
||||
region.Top = windowRect.Height - clientRect.Height + windowRect.Top - windowWithShadowRect.Top;
|
||||
region.Right = region.Left + clientRect.Width;
|
||||
region.Bottom = region.Top + clientRect.Height;
|
||||
region.Front = 0;
|
||||
region.Back = 1;
|
||||
|
||||
return region;
|
||||
}
|
||||
|
||||
public CaptureImageRes? Capture()
|
||||
{
|
||||
if (_hWnd == nint.Zero)
|
||||
try
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
lock (LockObject)
|
||||
{
|
||||
NativeMethods.DwmGetDxSharedSurface(_hWnd, out var phSurface, out _, out _, out _, out _);
|
||||
if (phSurface == nint.Zero)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
if (_d3dDevice == null)
|
||||
{
|
||||
Debug.WriteLine("D3Device is null.");
|
||||
return null;
|
||||
}
|
||||
|
||||
using var surfaceTexture = _d3dDevice.OpenSharedResource<Texture2D>(phSurface);
|
||||
using var stagingTexture = CreateStagingTexture(surfaceTexture, _d3dDevice);
|
||||
var mat = stagingTexture.CreateMat(_d3dDevice, surfaceTexture, _region);
|
||||
if (mat == null)
|
||||
|
||||
if (_stagingTexture == null || _surfaceWidth != surfaceTexture.Description.Width ||
|
||||
_surfaceHeight != surfaceTexture.Description.Height)
|
||||
{
|
||||
return null;
|
||||
_stagingTexture?.Dispose();
|
||||
_stagingTexture = null;
|
||||
_surfaceWidth = surfaceTexture.Description.Width;
|
||||
_surfaceHeight = surfaceTexture.Description.Height;
|
||||
_region = GetGameScreenRegion(_hWnd);
|
||||
}
|
||||
var bgrMat = new Mat();
|
||||
Cv2.CvtColor(mat, bgrMat, ColorConversionCodes.BGRA2BGR);
|
||||
return CaptureImageRes.BuildNullable(bgrMat);
|
||||
|
||||
_stagingTexture ??= Direct3D11Helper.CreateStagingTexture(_d3dDevice, _surfaceWidth, _surfaceHeight, _region);
|
||||
var mat = _stagingTexture.CreateMat(_d3dDevice, surfaceTexture, _region);
|
||||
return mat;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Texture2D CreateStagingTexture(Texture2D surfaceTexture, Device device)
|
||||
{
|
||||
return new Texture2D(device, new Texture2DDescription
|
||||
catch (SharpDXException e)
|
||||
{
|
||||
Width = _region == null ? surfaceTexture.Description.Width : _region.Value.Right - _region.Value.Left,
|
||||
Height = _region == null ? surfaceTexture.Description.Height : _region.Value.Bottom - _region.Value.Top,
|
||||
MipLevels = 1,
|
||||
ArraySize = 1,
|
||||
Format = Format.B8G8R8A8_UNorm,
|
||||
Usage = ResourceUsage.Staging,
|
||||
SampleDescription = new SampleDescription(1, 0),
|
||||
BindFlags = BindFlags.None,
|
||||
CpuAccessFlags = CpuAccessFlags.Read,
|
||||
OptionFlags = ResourceOptionFlags.None
|
||||
});
|
||||
}
|
||||
Debug.WriteLine($"SharpDXException: {e.Descriptor}");
|
||||
_d3dDevice?.Dispose();
|
||||
_d3dDevice = new Device(SharpDX.Direct3D.DriverType.Hardware, DeviceCreationFlags.BgraSupport);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
lock (LockObject)
|
||||
{
|
||||
_hWnd = nint.Zero;
|
||||
_stagingTexture?.Dispose();
|
||||
_stagingTexture = null;
|
||||
_d3dDevice?.Dispose();
|
||||
_d3dDevice = null;
|
||||
_hWnd = 0;
|
||||
IsCapturing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SharpDX.D3DCompiler" Version="4.2.0" />
|
||||
<PackageReference Include="SharpDX.Direct3D11" Version="4.2.0" />
|
||||
<PackageReference Include="SharpDX.DirectInput" Version="4.2.0" />
|
||||
<PackageReference Include="Vanara.PInvoke.DwmApi" Version="4.1.3" />
|
||||
|
||||
@@ -12,9 +12,9 @@ public class GameCaptureFactory
|
||||
return mode switch
|
||||
{
|
||||
CaptureModes.BitBlt => new BitBlt.BitBltCapture(),
|
||||
// CaptureModes.BitBlt => new BitBlt.BitBltOldCapture(),
|
||||
CaptureModes.WindowsGraphicsCapture => new Graphics.GraphicsCapture(),
|
||||
CaptureModes.DwmGetDxSharedSurface => new DwmSharedSurface.SharedSurfaceCapture(),
|
||||
CaptureModes.WindowsGraphicsCapture => new Graphics.GraphicsCapture(),
|
||||
CaptureModes.WindowsGraphicsCaptureHdr => new Graphics.GraphicsCapture(true),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,31 +7,31 @@ using Windows.Graphics.Capture;
|
||||
using Windows.Graphics.DirectX;
|
||||
using Windows.Graphics.DirectX.Direct3D11;
|
||||
using OpenCvSharp;
|
||||
using SharpDX.DXGI;
|
||||
using Device = SharpDX.Direct3D11.Device;
|
||||
using MapFlags = SharpDX.Direct3D11.MapFlags;
|
||||
using SharpDX;
|
||||
using SharpDX.D3DCompiler;
|
||||
|
||||
namespace Fischless.GameCapture.Graphics;
|
||||
|
||||
public class GraphicsCapture : IGameCapture
|
||||
public class GraphicsCapture(bool captureHdr = false) : IGameCapture
|
||||
{
|
||||
private nint _hWnd;
|
||||
|
||||
private Direct3D11CaptureFramePool _captureFramePool = null!;
|
||||
private GraphicsCaptureItem _captureItem = null!;
|
||||
private Direct3D11CaptureFramePool? _captureFramePool;
|
||||
private GraphicsCaptureItem? _captureItem;
|
||||
|
||||
private GraphicsCaptureSession _captureSession = null!;
|
||||
private GraphicsCaptureSession? _captureSession;
|
||||
|
||||
private IDirect3DDevice _d3dDevice = null!;
|
||||
private IDirect3DDevice? _d3dDevice;
|
||||
|
||||
public bool IsCapturing { get; private set; }
|
||||
|
||||
private ResourceRegion? _region;
|
||||
|
||||
// HDR相关
|
||||
private bool _isHdrEnabled;
|
||||
private DirectXPixelFormat _pixelFormat = DirectXPixelFormat.B8G8R8A8UIntNormalized;
|
||||
|
||||
private bool _isHdrEnabled = captureHdr;
|
||||
private DirectXPixelFormat _pixelFormat;
|
||||
private Texture2D? _hdrOutputTexture;
|
||||
private ComputeShader? _hdrComputeShader;
|
||||
|
||||
// 最新帧的存储
|
||||
private Mat? _latestFrame;
|
||||
@@ -40,11 +40,15 @@ public class GraphicsCapture : IGameCapture
|
||||
// 用于获取帧数据的临时纹理和暂存资源
|
||||
private Texture2D? _stagingTexture;
|
||||
|
||||
private long _lastFrameTime = 0;
|
||||
private long _lastFrameTime;
|
||||
|
||||
private readonly Stopwatch _frameTimer = new Stopwatch();
|
||||
private readonly Stopwatch _frameTimer = new();
|
||||
|
||||
public void Dispose() => Stop();
|
||||
public void Dispose()
|
||||
{
|
||||
Stop();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public void Start(nint hWnd, Dictionary<string, object>? settings = null)
|
||||
{
|
||||
@@ -65,16 +69,33 @@ public class GraphicsCapture : IGameCapture
|
||||
// 创建D3D设备
|
||||
_d3dDevice = Direct3D11Helper.CreateDevice();
|
||||
|
||||
// 检测HDR状态
|
||||
_isHdrEnabled = false;
|
||||
_pixelFormat = _isHdrEnabled ? DirectXPixelFormat.R16G16B16A16Float : DirectXPixelFormat.B8G8R8A8UIntNormalized;
|
||||
|
||||
// 创建帧池
|
||||
_captureFramePool = Direct3D11CaptureFramePool.Create(
|
||||
_d3dDevice,
|
||||
_pixelFormat,
|
||||
2,
|
||||
_captureItem.Size);
|
||||
try
|
||||
{
|
||||
if (!_isHdrEnabled)
|
||||
{
|
||||
// 不处理 HDR,直接抛异常走 SDR 分支
|
||||
throw new Exception();
|
||||
}
|
||||
|
||||
_pixelFormat = DirectXPixelFormat.R16G16B16A16Float;
|
||||
_captureFramePool = Direct3D11CaptureFramePool.CreateFreeThreaded(
|
||||
_d3dDevice,
|
||||
_pixelFormat,
|
||||
2,
|
||||
_captureItem.Size);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Fallback
|
||||
_pixelFormat = DirectXPixelFormat.B8G8R8A8UIntNormalized;
|
||||
_captureFramePool = Direct3D11CaptureFramePool.CreateFreeThreaded(
|
||||
_d3dDevice,
|
||||
_pixelFormat,
|
||||
2,
|
||||
_captureItem.Size);
|
||||
_isHdrEnabled = false;
|
||||
}
|
||||
|
||||
|
||||
_captureItem.Closed += CaptureItemOnClosed;
|
||||
@@ -82,13 +103,13 @@ public class GraphicsCapture : IGameCapture
|
||||
|
||||
_captureSession = _captureFramePool.CreateCaptureSession(_captureItem);
|
||||
if (ApiInformation.IsPropertyPresent("Windows.Graphics.Capture.GraphicsCaptureSession",
|
||||
"IsCursorCaptureEnabled"))
|
||||
nameof(GraphicsCaptureSession.IsCursorCaptureEnabled)))
|
||||
{
|
||||
_captureSession.IsCursorCaptureEnabled = false;
|
||||
}
|
||||
|
||||
if (ApiInformation.IsWriteablePropertyPresent("Windows.Graphics.Capture.GraphicsCaptureSession",
|
||||
"IsBorderRequired"))
|
||||
nameof(GraphicsCaptureSession.IsBorderRequired)))
|
||||
{
|
||||
_captureSession.IsBorderRequired = false;
|
||||
}
|
||||
@@ -111,186 +132,124 @@ public class GraphicsCapture : IGameCapture
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
ResourceRegion region = new();
|
||||
DwmApi.DwmGetWindowAttribute<RECT>(hWnd, DwmApi.DWMWINDOWATTRIBUTE.DWMWA_EXTENDED_FRAME_BOUNDS,
|
||||
out var windowRect);
|
||||
User32.GetClientRect(_hWnd, out var clientRect);
|
||||
//POINT point = default; // 这个点和 DwmGetWindowAttribute 结果差1
|
||||
//User32.ClientToScreen(hWnd, ref point);
|
||||
POINT point = default;
|
||||
User32.ClientToScreen(_hWnd, ref point);
|
||||
|
||||
region.Left = 0;
|
||||
region.Top = windowRect.Height - clientRect.Height;
|
||||
region.Right = clientRect.Width;
|
||||
region.Bottom = windowRect.Height;
|
||||
region.Left = point.X > windowRect.Left ? point.X - windowRect.Left : 0;
|
||||
region.Top = point.Y > windowRect.Top ? point.Y - windowRect.Top : 0;
|
||||
region.Right = region.Left + clientRect.Width;
|
||||
region.Bottom = region.Top + clientRect.Height;
|
||||
region.Front = 0;
|
||||
region.Back = 1;
|
||||
|
||||
return region;
|
||||
}
|
||||
|
||||
public static bool IsHdrEnabled(nint hWnd)
|
||||
private Texture2D ProcessHdrTexture(Texture2D hdrTexture)
|
||||
{
|
||||
try
|
||||
{
|
||||
var hdc = User32.GetDC(hWnd);
|
||||
if (hdc != IntPtr.Zero)
|
||||
{
|
||||
int bitsPerPixel = Gdi32.GetDeviceCaps(hdc, Gdi32.DeviceCap.BITSPIXEL);
|
||||
User32.ReleaseDC(hWnd, hdc);
|
||||
var device = hdrTexture.Device;
|
||||
var context = device.ImmediateContext;
|
||||
|
||||
// 如果位深大于等于32位,认为支持HDR
|
||||
return bitsPerPixel >= 32;
|
||||
}
|
||||
var width = hdrTexture.Description.Width;
|
||||
var height = hdrTexture.Description.Height;
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
_hdrOutputTexture ??= Direct3D11Helper.CreateOutputTexture(device, width, height);
|
||||
_hdrComputeShader ??= new ComputeShader(device, ShaderBytecode.Compile(HdrToSdrShader.Content, "CS_HDRtoSDR", "cs_5_0"));
|
||||
|
||||
private Texture2D CreateStagingTexture(Direct3D11CaptureFrame frame, Device device)
|
||||
{
|
||||
// 创建可以用于CPU读取的暂存纹理
|
||||
var textureDesc = new Texture2DDescription
|
||||
{
|
||||
CpuAccessFlags = CpuAccessFlags.Read,
|
||||
BindFlags = BindFlags.None,
|
||||
Format = _isHdrEnabled ? Format.R16G16B16A16_Float : Format.B8G8R8A8_UNorm,
|
||||
Width = _region == null ? frame.ContentSize.Width : _region.Value.Right - _region.Value.Left,
|
||||
Height = _region == null ? frame.ContentSize.Height : _region.Value.Bottom - _region.Value.Top,
|
||||
OptionFlags = ResourceOptionFlags.None,
|
||||
MipLevels = 1,
|
||||
ArraySize = 1,
|
||||
SampleDescription = { Count = 1, Quality = 0 },
|
||||
Usage = ResourceUsage.Staging
|
||||
};
|
||||
using var inputSrv = new ShaderResourceView(device, hdrTexture);
|
||||
using var outputUav = new UnorderedAccessView(device, _hdrOutputTexture);
|
||||
|
||||
return new Texture2D(device, textureDesc);
|
||||
context.ComputeShader.Set(_hdrComputeShader);
|
||||
context.ComputeShader.SetShaderResource(0, inputSrv);
|
||||
context.ComputeShader.SetUnorderedAccessView(0, outputUav);
|
||||
|
||||
var threadGroupCountX = (int)Math.Ceiling(width / 16.0);
|
||||
var threadGroupCountY = (int)Math.Ceiling(height / 16.0);
|
||||
|
||||
context.Dispatch(threadGroupCountX, threadGroupCountY, 1);
|
||||
|
||||
return _hdrOutputTexture;
|
||||
}
|
||||
|
||||
private void OnFrameArrived(Direct3D11CaptureFramePool sender, object args)
|
||||
{
|
||||
using var frame = sender.TryGetNextFrame();
|
||||
if (frame == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 限制最高处理帧率为62fps
|
||||
if (_frameTimer.ElapsedMilliseconds - _lastFrameTime < 16)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_lastFrameTime = _frameTimer.ElapsedMilliseconds;
|
||||
|
||||
var frameSize = _captureItem.Size;
|
||||
|
||||
// 检查帧大小是否变化 // 不会被访问到的代码
|
||||
if (frameSize.Width != frame.ContentSize.Width ||
|
||||
frameSize.Height != frame.ContentSize.Height)
|
||||
{
|
||||
frameSize = frame.ContentSize;
|
||||
_captureFramePool.Recreate(
|
||||
_d3dDevice,
|
||||
_pixelFormat,
|
||||
2,
|
||||
frameSize
|
||||
);
|
||||
_stagingTexture = null;
|
||||
}
|
||||
|
||||
// 从捕获的帧创建一个可以被访问的纹理
|
||||
using var surfaceTexture = Direct3D11Helper.CreateSharpDXTexture2D(frame.Surface);
|
||||
var d3dDevice = surfaceTexture.Device;
|
||||
|
||||
_stagingTexture ??= CreateStagingTexture(frame, d3dDevice);
|
||||
var stagingTexture = _stagingTexture;
|
||||
|
||||
// 将捕获的纹理复制到暂存纹理
|
||||
if (_region != null)
|
||||
{
|
||||
d3dDevice.ImmediateContext.CopySubresourceRegion(surfaceTexture, 0, _region, stagingTexture, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
d3dDevice.ImmediateContext.CopyResource(surfaceTexture, stagingTexture);
|
||||
}
|
||||
|
||||
|
||||
// 映射纹理以便CPU读取
|
||||
var dataBox = d3dDevice.ImmediateContext.MapSubresource(
|
||||
stagingTexture,
|
||||
0,
|
||||
MapMode.Read,
|
||||
MapFlags.None);
|
||||
|
||||
// 使用写锁更新最新帧
|
||||
_frameAccessLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
// 创建一个新的Mat
|
||||
var newFrame = Mat.FromPixelData(stagingTexture.Description.Height, stagingTexture.Description.Width,
|
||||
_isHdrEnabled ? MatType.MakeType(7, 4) : MatType.CV_8UC4, dataBox.DataPointer);
|
||||
|
||||
// 如果是HDR,进行HDR到SDR的转换
|
||||
if (_isHdrEnabled)
|
||||
if (_hWnd == 0)
|
||||
{
|
||||
// rgb -> bgr
|
||||
newFrame = ConvertHdrToSdr(newFrame);
|
||||
return;
|
||||
}
|
||||
|
||||
using var frame = sender.TryGetNextFrame();
|
||||
if (frame == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 限制最高处理帧率为62fps
|
||||
if (_frameTimer.ElapsedMilliseconds - _lastFrameTime < 16)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_lastFrameTime = _frameTimer.ElapsedMilliseconds;
|
||||
|
||||
var frameSize = _captureItem!.Size;
|
||||
|
||||
// 检查帧大小是否变化
|
||||
if (frameSize.Width != frame.ContentSize.Width || frameSize.Height != frame.ContentSize.Height)
|
||||
{
|
||||
frameSize = frame.ContentSize;
|
||||
_captureFramePool!.Recreate(
|
||||
_d3dDevice,
|
||||
_pixelFormat,
|
||||
2,
|
||||
frameSize
|
||||
);
|
||||
_stagingTexture?.Dispose();
|
||||
_stagingTexture = null;
|
||||
_region = GetGameScreenRegion(_hWnd);
|
||||
}
|
||||
|
||||
// 使用写锁更新最新帧
|
||||
_frameAccessLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
// 释放之前的帧
|
||||
// 从捕获的帧创建一个可以被访问的纹理
|
||||
using var surfaceTexture = Direct3D11Helper.CreateSharpDXTexture2D(frame.Surface);
|
||||
var sourceTexture = _isHdrEnabled ? ProcessHdrTexture(surfaceTexture) : surfaceTexture;
|
||||
var d3dDevice = surfaceTexture.Device;
|
||||
|
||||
_stagingTexture ??= Direct3D11Helper.CreateStagingTexture(d3dDevice, frame.ContentSize.Width, frame.ContentSize.Height, _region);
|
||||
var mat = _stagingTexture.CreateMat(d3dDevice, sourceTexture, _region);
|
||||
|
||||
// 释放之前的帧,然后更新
|
||||
_latestFrame?.Dispose();
|
||||
// 克隆新帧以保持对其的引用(因为dataBox.DataPointer将被释放)
|
||||
_latestFrame = newFrame.Clone();
|
||||
_latestFrame = mat;
|
||||
}
|
||||
finally
|
||||
catch (SharpDXException e)
|
||||
{
|
||||
newFrame.Dispose();
|
||||
_frameAccessLock.ExitWriteLock();
|
||||
Debug.WriteLine($"SharpDXException: {e.Descriptor}");
|
||||
_latestFrame = null;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
// 取消映射纹理
|
||||
d3dDevice.ImmediateContext.UnmapSubresource(surfaceTexture, 0);
|
||||
_frameAccessLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
private static Mat ConvertHdrToSdr(Mat hdrMat)
|
||||
{
|
||||
// 创建一个目标 8UC4 Mat
|
||||
Mat sdkMat = new Mat(hdrMat.Size(), MatType.CV_8UC4);
|
||||
|
||||
// 将 32FC4 缩放到 0-255 范围并转换为 8UC4
|
||||
// 注意:这种简单缩放可能不会保留 HDR 的所有细节
|
||||
hdrMat.ConvertTo(sdkMat, MatType.CV_8UC4, 255.0);
|
||||
|
||||
// 将 HDR 的 RGB 通道转换为 BGR
|
||||
Cv2.CvtColor(sdkMat, sdkMat, ColorConversionCodes.RGBA2BGRA);
|
||||
|
||||
return sdkMat;
|
||||
}
|
||||
|
||||
public CaptureImageRes? Capture()
|
||||
public Mat? Capture()
|
||||
{
|
||||
// 使用读锁获取最新帧
|
||||
_frameAccessLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
// 如果没有可用帧则返回null
|
||||
if (_latestFrame == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 返回最新帧的副本(这里我们必须克隆,因为Mat是不线程安全的)
|
||||
return CaptureImageRes.BuildNullable(_latestFrame.Clone());
|
||||
return _latestFrame?.Clone();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -300,20 +259,33 @@ public class GraphicsCapture : IGameCapture
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
_captureSession?.Dispose();
|
||||
_captureFramePool?.Dispose();
|
||||
_captureSession = null!;
|
||||
_captureFramePool = null!;
|
||||
_captureItem = null!;
|
||||
_stagingTexture?.Dispose();
|
||||
_d3dDevice?.Dispose();
|
||||
|
||||
_hWnd = IntPtr.Zero;
|
||||
IsCapturing = false;
|
||||
_frameAccessLock.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_captureSession?.Dispose();
|
||||
_captureSession = null;
|
||||
_captureFramePool?.Dispose();
|
||||
_captureFramePool = null;
|
||||
_captureItem = null;
|
||||
_stagingTexture?.Dispose();
|
||||
_stagingTexture = null;
|
||||
_hdrOutputTexture?.Dispose();
|
||||
_hdrOutputTexture = null;
|
||||
_hdrComputeShader?.Dispose();
|
||||
_hdrComputeShader = null;
|
||||
_d3dDevice?.Dispose();
|
||||
_d3dDevice = null;
|
||||
_hWnd = 0;
|
||||
IsCapturing = false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_frameAccessLock.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureItemOnClosed(GraphicsCaptureItem sender, object args)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Windows.Graphics.DirectX.Direct3D11;
|
||||
using SharpDX.Direct3D11;
|
||||
using SharpDX.DXGI;
|
||||
using WinRT;
|
||||
using Device = SharpDX.Direct3D11.Device;
|
||||
|
||||
namespace Fischless.GameCapture.Graphics.Helpers;
|
||||
|
||||
public static class Direct3D11Helper
|
||||
{
|
||||
internal static Guid IInspectable = new("AF86E2E0-B12D-4c6a-9C5A-D7AA65101E90");
|
||||
internal static Guid ID3D11Resource = new("dc8e63f3-d12b-4952-b47b-5e45026a862d");
|
||||
internal static Guid IDXGIAdapter3 = new("645967A4-1392-4310-A798-8053CE3E93FD");
|
||||
internal static Guid ID3D11Device = new("db6f6ddb-ac77-4e88-8253-819df9bbf140");
|
||||
internal static Guid ID3D11Texture2D = new("6f15aaf2-d208-4e89-9ab4-489535d34f9c");
|
||||
|
||||
[ComImport]
|
||||
@@ -31,33 +30,23 @@ public static class Direct3D11Helper
|
||||
)]
|
||||
static extern uint CreateDirect3D11DeviceFromDXGIDevice(nint dxgiDevice, out nint graphicsDevice);
|
||||
|
||||
[DllImport(
|
||||
"d3d11.dll",
|
||||
EntryPoint = "CreateDirect3D11SurfaceFromDXGISurface",
|
||||
SetLastError = true,
|
||||
CharSet = CharSet.Unicode,
|
||||
ExactSpelling = true,
|
||||
CallingConvention = CallingConvention.StdCall
|
||||
)]
|
||||
static extern uint CreateDirect3D11SurfaceFromDXGISurface(nint dxgiSurface, out nint graphicsSurface);
|
||||
|
||||
public static IDirect3DDevice CreateDevice()
|
||||
{
|
||||
return CreateDevice(false);
|
||||
}
|
||||
|
||||
private static SharpDX.Direct3D11.Device? d3dDevice;
|
||||
private static Device? d3dDevice;
|
||||
|
||||
public static IDirect3DDevice CreateDevice(bool useWARP)
|
||||
{
|
||||
d3dDevice ??= new SharpDX.Direct3D11.Device(
|
||||
d3dDevice ??= new Device(
|
||||
useWARP ? SharpDX.Direct3D.DriverType.Software : SharpDX.Direct3D.DriverType.Hardware,
|
||||
SharpDX.Direct3D11.DeviceCreationFlags.BgraSupport);
|
||||
DeviceCreationFlags.BgraSupport);
|
||||
var device = CreateDirect3DDeviceFromSharpDXDevice(d3dDevice);
|
||||
return device;
|
||||
}
|
||||
|
||||
public static IDirect3DDevice CreateDirect3DDeviceFromSharpDXDevice(SharpDX.Direct3D11.Device d3dDevice)
|
||||
public static IDirect3DDevice CreateDirect3DDeviceFromSharpDXDevice(Device d3dDevice)
|
||||
{
|
||||
IDirect3DDevice device = default!;
|
||||
|
||||
@@ -77,39 +66,45 @@ public static class Direct3D11Helper
|
||||
return device;
|
||||
}
|
||||
|
||||
public static IDirect3DSurface CreateDirect3DSurfaceFromSharpDXTexture(SharpDX.Direct3D11.Texture2D texture)
|
||||
{
|
||||
IDirect3DSurface surface = default!;
|
||||
|
||||
// Acquire the DXGI interface for the Direct3D surface.
|
||||
using (var dxgiSurface = texture.QueryInterface<SharpDX.DXGI.Surface>())
|
||||
{
|
||||
// Wrap the native device using a WinRT interop object.
|
||||
uint hr = CreateDirect3D11SurfaceFromDXGISurface(dxgiSurface.NativePointer, out nint pUnknown);
|
||||
|
||||
if (hr == 0)
|
||||
{
|
||||
surface = Marshal.GetObjectForIUnknown(pUnknown) as IDirect3DSurface;
|
||||
Marshal.Release(pUnknown);
|
||||
}
|
||||
}
|
||||
|
||||
return surface;
|
||||
}
|
||||
|
||||
public static SharpDX.Direct3D11.Device CreateSharpDXDevice(IDirect3DDevice device)
|
||||
{
|
||||
var access = device.As<IDirect3DDxgiInterfaceAccess>();
|
||||
var d3dPointer = access.GetInterface(ID3D11Device);
|
||||
var d3dDevice = new SharpDX.Direct3D11.Device(d3dPointer);
|
||||
return d3dDevice;
|
||||
}
|
||||
|
||||
public static SharpDX.Direct3D11.Texture2D CreateSharpDXTexture2D(IDirect3DSurface surface)
|
||||
public static Texture2D CreateSharpDXTexture2D(IDirect3DSurface surface)
|
||||
{
|
||||
var access = surface.As<IDirect3DDxgiInterfaceAccess>();
|
||||
var d3dPointer = access.GetInterface(ID3D11Texture2D);
|
||||
var d3dSurface = new SharpDX.Direct3D11.Texture2D(d3dPointer);
|
||||
var d3dSurface = new Texture2D(d3dPointer);
|
||||
return d3dSurface;
|
||||
}
|
||||
|
||||
public static Texture2D CreateStagingTexture(Device device, int width, int height, ResourceRegion? region)
|
||||
{
|
||||
return new Texture2D(device, new Texture2DDescription
|
||||
{
|
||||
Width = region == null ? width : region.Value.Right - region.Value.Left,
|
||||
Height = region == null ? height : region.Value.Bottom - region.Value.Top,
|
||||
MipLevels = 1,
|
||||
ArraySize = 1,
|
||||
Format = Format.B8G8R8A8_UNorm,
|
||||
Usage = ResourceUsage.Staging,
|
||||
SampleDescription = new SampleDescription(1, 0),
|
||||
BindFlags = BindFlags.None,
|
||||
CpuAccessFlags = CpuAccessFlags.Read,
|
||||
OptionFlags = ResourceOptionFlags.None
|
||||
});
|
||||
}
|
||||
|
||||
public static Texture2D CreateOutputTexture(Device device, int width, int height)
|
||||
{
|
||||
return new Texture2D(device, new Texture2DDescription
|
||||
{
|
||||
Width = width,
|
||||
Height = height,
|
||||
MipLevels = 1,
|
||||
ArraySize = 1,
|
||||
Format = Format.B8G8R8A8_UNorm,
|
||||
Usage = ResourceUsage.Default,
|
||||
SampleDescription = new SampleDescription(1, 0),
|
||||
BindFlags = BindFlags.UnorderedAccess | BindFlags.ShaderResource,
|
||||
CpuAccessFlags = CpuAccessFlags.None,
|
||||
OptionFlags = ResourceOptionFlags.None
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
27
Fischless.GameCapture/Graphics/Helpers/HdrToSdrShader.cs
Normal file
27
Fischless.GameCapture/Graphics/Helpers/HdrToSdrShader.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
namespace Fischless.GameCapture.Graphics.Helpers;
|
||||
|
||||
public static class HdrToSdrShader
|
||||
{
|
||||
public static string Content =>
|
||||
"""
|
||||
// HLSL Compute Shader
|
||||
Texture2D<half4> hdrTexture : register(t0);
|
||||
RWTexture2D<unorm float4> sdrTexture : register(u0);
|
||||
|
||||
[numthreads(16, 16, 1)]
|
||||
void CS_HDRtoSDR(uint3 id : SV_DispatchThreadID)
|
||||
{
|
||||
// Load color
|
||||
half4 hdrColor = hdrTexture[id.xy];
|
||||
|
||||
// HDR -> SDR (exposure)
|
||||
float4 exposedColor = float4(hdrColor.rgb * 0.25, hdrColor.a);
|
||||
|
||||
// Linear RGB -> sRGB
|
||||
float4 srgbColor = float4(pow(exposedColor.rgb, 1 / 2.2), exposedColor.a);
|
||||
|
||||
// Store color
|
||||
sdrTexture[id.xy] = (unorm float4)saturate(srgbColor);
|
||||
}
|
||||
""";
|
||||
}
|
||||
@@ -1,75 +1,12 @@
|
||||
using SharpDX;
|
||||
using SharpDX.Direct3D11;
|
||||
using SharpDX.DXGI;
|
||||
using SharpDX.Direct3D11;
|
||||
using System.Diagnostics;
|
||||
using System.Drawing.Imaging;
|
||||
using Windows.Graphics.Capture;
|
||||
using OpenCvSharp;
|
||||
|
||||
namespace Fischless.GameCapture.Graphics.Helpers;
|
||||
|
||||
public static class Texture2DExtensions
|
||||
{
|
||||
public static Bitmap? ToBitmap(this Direct3D11CaptureFrame frame, ResourceRegion? region = null)
|
||||
{
|
||||
var texture2dBitmap = Direct3D11Helper.CreateSharpDXTexture2D(frame.Surface);
|
||||
|
||||
var d3dDevice = texture2dBitmap.Device;
|
||||
|
||||
// Create texture copy
|
||||
var staging = new Texture2D(d3dDevice, new Texture2DDescription
|
||||
{
|
||||
Width = region == null ? frame.ContentSize.Width : region.Value.Right - region.Value.Left,
|
||||
Height = region == null ? frame.ContentSize.Height : region.Value.Bottom - region.Value.Top,
|
||||
MipLevels = 1,
|
||||
ArraySize = 1,
|
||||
Format = texture2dBitmap.Description.Format,
|
||||
Usage = ResourceUsage.Staging,
|
||||
SampleDescription = new SampleDescription(1, 0),
|
||||
BindFlags = BindFlags.None,
|
||||
CpuAccessFlags = CpuAccessFlags.Read,
|
||||
OptionFlags = ResourceOptionFlags.None
|
||||
});
|
||||
|
||||
return staging.CreateBitmap(d3dDevice, texture2dBitmap, region);
|
||||
}
|
||||
|
||||
public static Bitmap? CreateBitmap(this Texture2D staging, SharpDX.Direct3D11.Device d3dDevice, Texture2D surfaceTexture, ResourceRegion? region = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Copy data
|
||||
if (region != null)
|
||||
{
|
||||
d3dDevice.ImmediateContext.CopySubresourceRegion(surfaceTexture, 0, region, staging, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
d3dDevice.ImmediateContext.CopyResource(surfaceTexture, staging);
|
||||
}
|
||||
|
||||
var dataBox = d3dDevice.ImmediateContext.MapSubresource(staging, 0, 0, MapMode.Read,
|
||||
SharpDX.Direct3D11.MapFlags.None,
|
||||
out DataStream stream);
|
||||
|
||||
var bitmap = new Bitmap(staging.Description.Width, staging.Description.Height, dataBox.RowPitch,
|
||||
PixelFormat.Format32bppArgb, dataBox.DataPointer);
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.WriteLine("Failed to copy texture to bitmap.");
|
||||
Debug.WriteLine(e.StackTrace);
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
staging.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public static Mat? CreateMat(this Texture2D staging, SharpDX.Direct3D11.Device d3dDevice, Texture2D surfaceTexture, ResourceRegion? region = null)
|
||||
public static Mat? CreateMat(this Texture2D staging, Device d3dDevice, Texture2D surfaceTexture, ResourceRegion? region = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -88,10 +25,18 @@ public static class Texture2DExtensions
|
||||
staging,
|
||||
0,
|
||||
MapMode.Read,
|
||||
SharpDX.Direct3D11.MapFlags.None);
|
||||
MapFlags.None);
|
||||
|
||||
var mat = Mat.FromPixelData(staging.Description.Height, staging.Description.Width, MatType.CV_8UC4, dataBox.DataPointer);
|
||||
return mat;
|
||||
try
|
||||
{
|
||||
using var mat = Mat.FromPixelData(staging.Description.Height, staging.Description.Width,
|
||||
MatType.CV_8UC4, dataBox.DataPointer, dataBox.RowPitch);
|
||||
return mat.CvtColor(ColorConversionCodes.BGRA2BGR);
|
||||
}
|
||||
finally
|
||||
{
|
||||
d3dDevice.ImmediateContext.UnmapSubresource(staging, 0);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -99,9 +44,5 @@ public static class Texture2DExtensions
|
||||
Debug.WriteLine(e.StackTrace);
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
staging.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ public interface IGameCapture : IDisposable
|
||||
|
||||
public void Start(nint hWnd, Dictionary<string, object>? settings = null);
|
||||
|
||||
public CaptureImageRes? Capture();
|
||||
public Mat? Capture();
|
||||
|
||||
public void Stop();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user