截图优化 (#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

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