截图优化 (#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:
Shatyuka
2025-05-11 01:17:18 +08:00
committed by GitHub
parent 5b3bac478d
commit 0bea2d095a
29 changed files with 670 additions and 759 deletions

View File

@@ -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;
}
}
}

View 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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();
}
}

View File

@@ -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("WindowsGraphicsCaptureHDR")]
[DefaultValue(3)]
WindowsGraphicsCaptureHdr = 3,
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}
}

View File

@@ -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" />

View File

@@ -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),
};
}

View File

@@ -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();
}
}
}

View File

@@ -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
});
}
}

View 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);
}
""";
}

View File

@@ -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();
}
}
}
}

View File

@@ -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();
}