142 lines
4.1 KiB
C#
142 lines
4.1 KiB
C#
using System.Runtime.InteropServices;
|
||
using OpenCvSharp;
|
||
using Serilog;
|
||
using SharpGen.Runtime;
|
||
using Vortice.Direct3D;
|
||
using Vortice.Direct3D11;
|
||
using Vortice.DXGI;
|
||
using Region = Poe2Trade.Core.Region;
|
||
|
||
namespace Poe2Trade.Screen;
|
||
|
||
public sealed class DesktopDuplication : IScreenCapture
|
||
{
|
||
private readonly ID3D11Device _device;
|
||
private readonly ID3D11DeviceContext _context;
|
||
private IDXGIOutputDuplication? _duplication;
|
||
private ID3D11Texture2D? _staging;
|
||
private int _stagingW, _stagingH;
|
||
private bool _needsRecreate;
|
||
|
||
public DesktopDuplication()
|
||
{
|
||
D3D11.D3D11CreateDevice(
|
||
null,
|
||
DriverType.Hardware,
|
||
DeviceCreationFlags.BgraSupport,
|
||
[FeatureLevel.Level_11_0],
|
||
out _device!,
|
||
out _context!).CheckError();
|
||
|
||
CreateDuplication();
|
||
}
|
||
|
||
private void CreateDuplication()
|
||
{
|
||
using var dxgiDevice = _device.QueryInterface<IDXGIDevice>();
|
||
using var adapter = dxgiDevice.GetAdapter();
|
||
adapter.EnumOutputs(0, out var output);
|
||
using var _ = output;
|
||
using var output1 = output.QueryInterface<IDXGIOutput1>();
|
||
_duplication = output1.DuplicateOutput(_device);
|
||
_needsRecreate = false;
|
||
Log.Debug("DXGI Desktop Duplication created");
|
||
}
|
||
|
||
public unsafe ScreenFrame? CaptureFrame()
|
||
{
|
||
if (_duplication == null) return null;
|
||
|
||
if (_needsRecreate)
|
||
{
|
||
try
|
||
{
|
||
_duplication.Dispose();
|
||
CreateDuplication();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Log.Warning(ex, "Failed to recreate DXGI duplication");
|
||
return null;
|
||
}
|
||
}
|
||
|
||
IDXGIResource? resource = null;
|
||
try
|
||
{
|
||
_duplication.AcquireNextFrame(100, out _, out resource);
|
||
}
|
||
catch (SharpGenException ex)
|
||
{
|
||
if (ex.ResultCode == Vortice.DXGI.ResultCode.AccessLost)
|
||
_needsRecreate = true;
|
||
return null;
|
||
}
|
||
|
||
var srcTexture = resource!.QueryInterface<ID3D11Texture2D>();
|
||
var desc = srcTexture.Description;
|
||
var w = (int)desc.Width;
|
||
var h = (int)desc.Height;
|
||
EnsureStaging(w, h);
|
||
|
||
_context.CopySubresourceRegion(_staging!, 0, 0, 0, 0, srcTexture, 0);
|
||
|
||
var mapped = _context.Map(_staging!, 0, MapMode.Read);
|
||
|
||
// GPU copy is complete once Map returns — release DXGI frame immediately
|
||
// so the DWM compositor can recycle the buffer (~0.5ms hold vs ~2.5ms before).
|
||
srcTexture.Dispose();
|
||
resource.Dispose();
|
||
_duplication!.ReleaseFrame();
|
||
|
||
// CPU copy from our staging texture (no DXGI dependency, ~1.5ms for 2560×1440)
|
||
var mat = new Mat(h, w, MatType.CV_8UC4);
|
||
unsafe
|
||
{
|
||
var src = (byte*)mapped.DataPointer;
|
||
var dst = (byte*)mat.Data;
|
||
var rowBytes = w * 4;
|
||
var pitch = (int)mapped.RowPitch;
|
||
for (var row = 0; row < h; row++)
|
||
Buffer.MemoryCopy(src + row * pitch, dst + row * rowBytes, rowBytes, rowBytes);
|
||
}
|
||
|
||
_context.Unmap(_staging!, 0);
|
||
return new ScreenFrame(mat, () => mat.Dispose());
|
||
}
|
||
|
||
public unsafe Mat? CaptureRegion(Region region)
|
||
{
|
||
using var frame = CaptureFrame();
|
||
if (frame == null) return null;
|
||
return frame.CropBgr(region);
|
||
}
|
||
|
||
private void EnsureStaging(int w, int h)
|
||
{
|
||
if (_staging != null && _stagingW == w && _stagingH == h) return;
|
||
_staging?.Dispose();
|
||
|
||
_staging = _device.CreateTexture2D(new Texture2DDescription
|
||
{
|
||
Width = (uint)w,
|
||
Height = (uint)h,
|
||
MipLevels = 1,
|
||
ArraySize = 1,
|
||
Format = Format.B8G8R8A8_UNorm,
|
||
SampleDescription = new SampleDescription(1, 0),
|
||
Usage = ResourceUsage.Staging,
|
||
CPUAccessFlags = CpuAccessFlags.Read,
|
||
});
|
||
_stagingW = w;
|
||
_stagingH = h;
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
_staging?.Dispose();
|
||
_duplication?.Dispose();
|
||
_context?.Dispose();
|
||
_device?.Dispose();
|
||
}
|
||
}
|