small refactor

This commit is contained in:
Boki 2026-02-15 16:32:15 -05:00
parent 5958d28ed8
commit 3fe7c0b37d
11 changed files with 11 additions and 237 deletions

View file

@ -0,0 +1,132 @@
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);
var mat = Mat.FromPixelData(h, w, MatType.CV_8UC4, mapped.DataPointer, (int)mapped.RowPitch);
var duplication = _duplication!;
return new ScreenFrame(mat, () =>
{
_context.Unmap(_staging!, 0);
srcTexture.Dispose();
resource.Dispose();
duplication.ReleaseFrame();
});
}
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();
}
}

View file

@ -0,0 +1,37 @@
namespace Poe2Trade.Screen;
public class FramePipeline : IDisposable
{
private readonly IScreenCapture _capture;
private readonly List<IFrameConsumer> _consumers = [];
public FramePipeline(IScreenCapture capture)
{
_capture = capture;
}
public IScreenCapture Capture => _capture;
public void AddConsumer(IFrameConsumer consumer) => _consumers.Add(consumer);
/// <summary>
/// Capture one frame, dispatch to all consumers in parallel, then dispose frame.
/// </summary>
public async Task ProcessOneFrame()
{
using var frame = _capture.CaptureFrame();
if (frame == null) return;
if (_consumers.Count == 1)
{
_consumers[0].Process(frame);
}
else
{
var tasks = _consumers.Select(c => Task.Run(() => c.Process(frame)));
await Task.WhenAll(tasks);
}
}
public void Dispose() => _capture.Dispose();
}

View file

@ -0,0 +1,54 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using OpenCvSharp;
using OpenCvSharp.Extensions;
using Region = Poe2Trade.Core.Region;
namespace Poe2Trade.Screen;
public sealed class GdiCapture : IScreenCapture
{
[DllImport("user32.dll")]
private static extern int GetSystemMetrics(int nIndex);
private const int SM_CXSCREEN = 0;
private const int SM_CYSCREEN = 1;
public ScreenFrame? CaptureFrame()
{
var w = GetSystemMetrics(SM_CXSCREEN);
var h = GetSystemMetrics(SM_CYSCREEN);
var bitmap = new Bitmap(w, h, PixelFormat.Format32bppArgb);
using (var g = Graphics.FromImage(bitmap))
{
g.CopyFromScreen(0, 0, 0, 0, new System.Drawing.Size(w, h),
CopyPixelOperation.SourceCopy);
}
var mat = BitmapConverter.ToMat(bitmap);
bitmap.Dispose();
// ToMat returns BGR or BGRA depending on pixel format; ensure BGRA
if (mat.Channels() == 3)
{
var bgra = new Mat();
Cv2.CvtColor(mat, bgra, ColorConversionCodes.BGR2BGRA);
mat.Dispose();
mat = bgra;
}
var ownedMat = mat;
return new ScreenFrame(ownedMat, () => ownedMat.Dispose());
}
public Mat? CaptureRegion(Region region)
{
using var frame = CaptureFrame();
if (frame == null) return null;
return frame.CropBgr(region);
}
public void Dispose() { }
}

View file

@ -0,0 +1,6 @@
namespace Poe2Trade.Screen;
public interface IFrameConsumer
{
void Process(ScreenFrame frame);
}

View file

@ -0,0 +1,10 @@
using OpenCvSharp;
using Region = Poe2Trade.Core.Region;
namespace Poe2Trade.Screen;
public interface IScreenCapture : IDisposable
{
Mat? CaptureRegion(Region region);
ScreenFrame? CaptureFrame();
}

View file

@ -10,6 +10,8 @@
<PackageReference Include="OpenCvSharp4.Extensions" Version="4.11.0.*" />
<PackageReference Include="OpenCvSharp4.runtime.win" Version="4.11.0.*" />
<PackageReference Include="System.Drawing.Common" Version="8.0.12" />
<PackageReference Include="Vortice.Direct3D11" Version="3.8.2" />
<PackageReference Include="Vortice.DXGI" Version="3.8.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Poe2Trade.Core\Poe2Trade.Core.csproj" />

View file

@ -0,0 +1,37 @@
using OpenCvSharp;
using Region = Poe2Trade.Core.Region;
namespace Poe2Trade.Screen;
public class ScreenFrame : IDisposable
{
private readonly Mat _bgraMat;
private readonly Action _disposeAction;
public ScreenFrame(Mat bgraMat, Action disposeAction)
{
_bgraMat = bgraMat;
_disposeAction = disposeAction;
}
public int Width => _bgraMat.Width;
public int Height => _bgraMat.Height;
/// <summary>
/// Crop region and convert to BGR. Returns a new Mat owned by the caller.
/// </summary>
public Mat CropBgr(Region region)
{
using var roi = new Mat(_bgraMat, new Rect(region.X, region.Y, region.Width, region.Height));
var bgr = new Mat();
Cv2.CvtColor(roi, bgr, ColorConversionCodes.BGRA2BGR);
return bgr;
}
/// <summary>
/// Read a single pixel (B, G, R, A) — useful for mode detection probes.
/// </summary>
public Vec4b PixelAt(int x, int y) => _bgraMat.At<Vec4b>(y, x);
public void Dispose() => _disposeAction();
}