From 6ea373f2c31f9694d0e1eda4bd015190b7e32974 Mon Sep 17 00:00:00 2001 From: Boki Date: Fri, 13 Feb 2026 18:13:38 -0500 Subject: [PATCH] full capture --- .../DesktopDuplication.cs | 68 +++---- src/Poe2Trade.Navigation/FramePipeline.cs | 37 ++++ src/Poe2Trade.Navigation/GdiCapture.cs | 44 ++++- src/Poe2Trade.Navigation/IFrameConsumer.cs | 6 + src/Poe2Trade.Navigation/IScreenCapture.cs | 1 + src/Poe2Trade.Navigation/MinimapCapture.cs | 169 ++++++++---------- .../NavigationExecutor.cs | 50 +++++- src/Poe2Trade.Navigation/ScreenFrame.cs | 37 ++++ src/Poe2Trade.Navigation/WgcCapture.cs | 50 +++--- .../ViewModels/MainWindowViewModel.cs | 2 +- 10 files changed, 291 insertions(+), 173 deletions(-) create mode 100644 src/Poe2Trade.Navigation/FramePipeline.cs create mode 100644 src/Poe2Trade.Navigation/IFrameConsumer.cs create mode 100644 src/Poe2Trade.Navigation/ScreenFrame.cs diff --git a/src/Poe2Trade.Navigation/DesktopDuplication.cs b/src/Poe2Trade.Navigation/DesktopDuplication.cs index b9aac3a..ee62d10 100644 --- a/src/Poe2Trade.Navigation/DesktopDuplication.cs +++ b/src/Poe2Trade.Navigation/DesktopDuplication.cs @@ -43,7 +43,7 @@ public sealed class DesktopDuplication : IScreenCapture Log.Debug("DXGI Desktop Duplication created"); } - public unsafe Mat? CaptureRegion(Region region) + public unsafe ScreenFrame? CaptureFrame() { if (_duplication == null) return null; @@ -70,52 +70,36 @@ public sealed class DesktopDuplication : IScreenCapture { if (ex.ResultCode == Vortice.DXGI.ResultCode.AccessLost) _needsRecreate = true; - // WaitTimeout is normal when screen hasn't changed return null; } - try + var srcTexture = resource!.QueryInterface(); + 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, () => { - using var srcTexture = resource!.QueryInterface(); - EnsureStaging(region.Width, region.Height); + _context.Unmap(_staging!, 0); + srcTexture.Dispose(); + resource.Dispose(); + duplication.ReleaseFrame(); + }); + } - // Copy only the region we need from the desktop texture - _context.CopySubresourceRegion( - _staging!, 0, 0, 0, 0, - srcTexture, 0, - new Vortice.Mathematics.Box(region.X, region.Y, 0, - region.X + region.Width, region.Y + region.Height, 1)); - - var mapped = _context.Map(_staging!, 0, MapMode.Read); - try - { - var mat = new Mat(region.Height, region.Width, MatType.CV_8UC4); - var rowBytes = region.Width * 4; - - for (var row = 0; row < region.Height; row++) - { - Buffer.MemoryCopy( - (void*)(mapped.DataPointer + row * mapped.RowPitch), - (void*)mat.Ptr(row), - rowBytes, rowBytes); - } - - // BGRA → BGR - var bgr = new Mat(); - Cv2.CvtColor(mat, bgr, ColorConversionCodes.BGRA2BGR); - mat.Dispose(); - return bgr; - } - finally - { - _context.Unmap(_staging!, 0); - } - } - finally - { - 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) diff --git a/src/Poe2Trade.Navigation/FramePipeline.cs b/src/Poe2Trade.Navigation/FramePipeline.cs new file mode 100644 index 0000000..a3a2739 --- /dev/null +++ b/src/Poe2Trade.Navigation/FramePipeline.cs @@ -0,0 +1,37 @@ +namespace Poe2Trade.Navigation; + +public class FramePipeline : IDisposable +{ + private readonly IScreenCapture _capture; + private readonly List _consumers = []; + + public FramePipeline(IScreenCapture capture) + { + _capture = capture; + } + + public IScreenCapture Capture => _capture; + + public void AddConsumer(IFrameConsumer consumer) => _consumers.Add(consumer); + + /// + /// Capture one frame, dispatch to all consumers in parallel, then dispose frame. + /// + 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(); +} diff --git a/src/Poe2Trade.Navigation/GdiCapture.cs b/src/Poe2Trade.Navigation/GdiCapture.cs index 44799f8..36881ce 100644 --- a/src/Poe2Trade.Navigation/GdiCapture.cs +++ b/src/Poe2Trade.Navigation/GdiCapture.cs @@ -1,5 +1,6 @@ using System.Drawing; using System.Drawing.Imaging; +using System.Runtime.InteropServices; using OpenCvSharp; using OpenCvSharp.Extensions; using Region = Poe2Trade.Core.Region; @@ -8,14 +9,45 @@ namespace Poe2Trade.Navigation; 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 bitmap = new Bitmap(region.Width, region.Height, PixelFormat.Format32bppArgb); - using var g = Graphics.FromImage(bitmap); - g.CopyFromScreen(region.X, region.Y, 0, 0, - new System.Drawing.Size(region.Width, region.Height), - CopyPixelOperation.SourceCopy); - return BitmapConverter.ToMat(bitmap); + using var frame = CaptureFrame(); + if (frame == null) return null; + return frame.CropBgr(region); } public void Dispose() { } diff --git a/src/Poe2Trade.Navigation/IFrameConsumer.cs b/src/Poe2Trade.Navigation/IFrameConsumer.cs new file mode 100644 index 0000000..4931e55 --- /dev/null +++ b/src/Poe2Trade.Navigation/IFrameConsumer.cs @@ -0,0 +1,6 @@ +namespace Poe2Trade.Navigation; + +public interface IFrameConsumer +{ + void Process(ScreenFrame frame); +} diff --git a/src/Poe2Trade.Navigation/IScreenCapture.cs b/src/Poe2Trade.Navigation/IScreenCapture.cs index faff0ce..b1e2d1b 100644 --- a/src/Poe2Trade.Navigation/IScreenCapture.cs +++ b/src/Poe2Trade.Navigation/IScreenCapture.cs @@ -6,4 +6,5 @@ namespace Poe2Trade.Navigation; public interface IScreenCapture : IDisposable { Mat? CaptureRegion(Region region); + ScreenFrame? CaptureFrame(); } diff --git a/src/Poe2Trade.Navigation/MinimapCapture.cs b/src/Poe2Trade.Navigation/MinimapCapture.cs index 308f006..da5630f 100644 --- a/src/Poe2Trade.Navigation/MinimapCapture.cs +++ b/src/Poe2Trade.Navigation/MinimapCapture.cs @@ -5,68 +5,62 @@ using Size = OpenCvSharp.Size; namespace Poe2Trade.Navigation; -public class MinimapCapture : IDisposable +public class MinimapCapture : IFrameConsumer, IDisposable { private readonly MinimapConfig _config; - private readonly IScreenCapture _backend; private readonly WallColorTracker _colorTracker; + private readonly IScreenCapture _backend; // kept for debug capture paths + private int _modeCheckCounter; private MinimapMode _detectedMode = MinimapMode.Overlay; + private MinimapFrame? _lastFrame; public MinimapMode DetectedMode => _detectedMode; + public MinimapFrame? LastFrame => _lastFrame; public event Action? ModeChanged; - public MinimapCapture(MinimapConfig config) + public MinimapCapture(MinimapConfig config, IScreenCapture backend) { _config = config; _colorTracker = new WallColorTracker(config.WallLoHSV, config.WallHiHSV); - _backend = CreateBackend(); + _backend = backend; } - private static IScreenCapture CreateBackend() + /// + /// IFrameConsumer entry point — called by FramePipeline with the shared screen frame. + /// + public void Process(ScreenFrame screen) { - // WGC primary → DXGI fallback → GDI last resort - try + // Auto-detect minimap mode every 10th frame via single pixel probe + if (++_modeCheckCounter >= 10) { - var wgc = new WgcCapture(); - Log.Information("Screen capture: WGC (Windows Graphics Capture)"); - return wgc; - } - catch (Exception ex) - { - Log.Warning(ex, "WGC unavailable, trying DXGI Desktop Duplication"); + _modeCheckCounter = 0; + var detected = DetectMinimapMode(screen); + if (detected != _detectedMode) + { + _detectedMode = detected; + Log.Information("Minimap mode switched to {Mode}", _detectedMode); + ResetAdaptation(); + ModeChanged?.Invoke(_detectedMode); + } } - try - { - var dxgi = new DesktopDuplication(); - Log.Information("Screen capture: DXGI Desktop Duplication"); - return dxgi; - } - catch (Exception ex) - { - Log.Warning(ex, "DXGI unavailable, falling back to GDI"); - } + var region = _detectedMode == MinimapMode.Overlay + ? _config.OverlayRegion + : _config.CornerRegion; - Log.Information("Screen capture: GDI (CopyFromScreen)"); - return new GdiCapture(); + using var bgr = screen.CropBgr(region); + if (bgr.Empty()) + return; + + var frame = ProcessBgr(bgr); + if (frame == null) return; + + var old = Interlocked.Exchange(ref _lastFrame, frame); + old?.Dispose(); } - public MinimapFrame? CaptureFrame() + private MinimapFrame? ProcessBgr(Mat bgr) { - // Auto-detect minimap mode every frame (just a 5x5 pixel check, negligible cost) - var detected = DetectMinimapMode(); - if (detected != _detectedMode) - { - _detectedMode = detected; - Log.Information("Minimap mode switched to {Mode}", _detectedMode); - ResetAdaptation(); - ModeChanged?.Invoke(_detectedMode); - } - - using var bgr = CaptureAndNormalize(_detectedMode); - if (bgr == null || bgr.Empty()) - return null; - using var hsv = new Mat(); Cv2.CvtColor(bgr, hsv, ColorConversionCodes.BGR2HSV); @@ -84,12 +78,10 @@ public class MinimapCapture : IDisposable if (_detectedMode == MinimapMode.Corner) { - // Corner minimap is clean — skip fog detection classified.SetTo(new Scalar((byte)MapCell.Wall), wallMask); } else { - // Overlay: fog detection needed (broad blue minus walls minus player) using var fogMask = BuildFogMask(hsv, wallMask, playerMask); classified.SetTo(new Scalar((byte)MapCell.Fog), fogMask); classified.SetTo(new Scalar((byte)MapCell.Wall), wallMask); @@ -101,7 +93,6 @@ public class MinimapCapture : IDisposable grayForCorr.SetTo(Scalar.Black, playerMask); // Corner mode: rescale classified + wall mask to match overlay scale - // Uses nearest-neighbor so discrete cell values (0-3) stay crisp if (_detectedMode == MinimapMode.Corner && Math.Abs(_config.CornerScale - 1.0) > 0.01) { var scaledSize = (int)Math.Round(frameSize * _config.CornerScale); @@ -133,36 +124,37 @@ public class MinimapCapture : IDisposable ); } - private Mat? CaptureAndNormalize(MinimapMode mode) - { - var region = mode == MinimapMode.Overlay - ? _config.OverlayRegion - : _config.CornerRegion; - - return _backend.CaptureRegion(region); - } - /// - /// Detect minimap mode by sampling a small patch at the corner minimap center. + /// Detect minimap mode by sampling pixels at the corner minimap center. /// If the pixel is close to #DE581B (orange player dot), corner minimap is active. /// - private MinimapMode DetectMinimapMode() + private MinimapMode DetectMinimapMode(ScreenFrame screen) { - // Capture a tiny 5x5 region at the corner center var cx = _config.CornerCenterX; var cy = _config.CornerCenterY; - var probe = new Poe2Trade.Core.Region(cx - 2, cy - 2, 5, 5); - using var patch = _backend.CaptureRegion(probe); - if (patch == null || patch.Empty()) - return _detectedMode; // keep current on failure - // Average the BGR values of the patch - var mean = Cv2.Mean(patch); - var b = mean.Val0; - var g = mean.Val1; - var r = mean.Val2; + // Bounds check + if (cx < 2 || cy < 2 || cx + 2 >= screen.Width || cy + 2 >= screen.Height) + return _detectedMode; - // #DE581B → R=222, G=88, B=27 — check if close (tolerance ~60 per channel) + // Average a 5x5 patch worth of pixels + double bSum = 0, gSum = 0, rSum = 0; + var count = 0; + for (var dy = -2; dy <= 2; dy++) + for (var dx = -2; dx <= 2; dx++) + { + var px = screen.PixelAt(cx + dx, cy + dy); + bSum += px.Item0; + gSum += px.Item1; + rSum += px.Item2; + count++; + } + + var b = bSum / count; + var g = gSum / count; + var r = rSum / count; + + // #DE581B → R=222, G=88, B=27 const int tol = 60; if (Math.Abs(r - 222) < tol && Math.Abs(g - 88) < tol && Math.Abs(b - 27) < tol) return MinimapMode.Corner; @@ -172,23 +164,19 @@ public class MinimapCapture : IDisposable private Mat BuildWallMask(Mat hsv, Mat playerMask, bool sample = false) { - // Use adapted range if available (narrows per-map), otherwise broad default var lo = _colorTracker.AdaptedLo ?? _config.WallLoHSV; var hi = _colorTracker.AdaptedHi ?? _config.WallHiHSV; var wallMask = new Mat(); Cv2.InRange(hsv, lo, hi, wallMask); - // Subtract player marker (orange overlaps blue range slightly on some maps) using var notPlayer = new Mat(); Cv2.BitwiseNot(playerMask, notPlayer); Cv2.BitwiseAnd(wallMask, notPlayer, wallMask); - // Sample from pure color-selected pixels BEFORE morphological ops if (sample) _colorTracker.SampleFrame(hsv, wallMask); - // Dilate to connect thin wall line fragments using var kernel = Cv2.GetStructuringElement(MorphShapes.Ellipse, new Size(3, 3)); Cv2.Dilate(wallMask, wallMask, kernel); @@ -198,11 +186,9 @@ public class MinimapCapture : IDisposable private Mat BuildFogMask(Mat hsv, Mat wallMask, Mat playerMask) { - // Broad blue detection (captures walls + fog + any blue) using var allBlue = new Mat(); Cv2.InRange(hsv, _config.FogLoHSV, _config.FogHiHSV, allBlue); - // Subtract player and walls → remaining blue is fog using var notPlayer = new Mat(); Cv2.BitwiseNot(playerMask, notPlayer); Cv2.BitwiseAnd(allBlue, notPlayer, allBlue); @@ -225,13 +211,12 @@ public class MinimapCapture : IDisposable using var centroids = new Mat(); var numLabels = Cv2.ConnectedComponentsWithStats(mask, labels, stats, centroids); - if (numLabels <= 1) return; // only background + if (numLabels <= 1) return; - // Clear mask, then re-add only components meeting min area mask.SetTo(Scalar.Black); for (var i = 1; i < numLabels; i++) { - var area = stats.At(i, 4); // CC_STAT_AREA + var area = stats.At(i, 4); if (area < minArea) continue; using var comp = new Mat(); @@ -242,10 +227,15 @@ public class MinimapCapture : IDisposable /// /// Capture a single frame and return the requested pipeline stage as PNG bytes. + /// Uses CaptureRegion directly (debug path, not the pipeline). /// public byte[]? CaptureStage(MinimapDebugStage stage) { - using var bgr = CaptureAndNormalize(_detectedMode); + var region = _detectedMode == MinimapMode.Overlay + ? _config.OverlayRegion + : _config.CornerRegion; + + using var bgr = _backend.CaptureRegion(region); if (bgr == null || bgr.Empty()) return null; if (stage == MinimapDebugStage.Raw) return EncodePng(bgr); @@ -272,11 +262,10 @@ public class MinimapCapture : IDisposable using var fogMask = BuildFogMask(hsv, wallMask, playerMask); if (stage == MinimapDebugStage.Fog) return EncodePng(fogMask); - // Classified (walls + fog + player — explored is tracked by WorldMap) using var classified = new Mat(bgr.Height, bgr.Width, MatType.CV_8UC3, Scalar.Black); - classified.SetTo(new Scalar(180, 140, 70), fogMask); // light blue for fog - classified.SetTo(new Scalar(26, 45, 61), wallMask); // brown for walls - classified.SetTo(new Scalar(0, 165, 255), playerMask); // orange for player + classified.SetTo(new Scalar(180, 140, 70), fogMask); + classified.SetTo(new Scalar(26, 45, 61), wallMask); + classified.SetTo(new Scalar(0, 165, 255), playerMask); return EncodePng(classified); } @@ -292,7 +281,12 @@ public class MinimapCapture : IDisposable public void SaveDebugCapture(string dir = "debug-minimap") { Directory.CreateDirectory(dir); - using var bgr = CaptureAndNormalize(_detectedMode); + + var region = _detectedMode == MinimapMode.Overlay + ? _config.OverlayRegion + : _config.CornerRegion; + + using var bgr = _backend.CaptureRegion(region); if (bgr == null || bgr.Empty()) return; using var hsv = new Mat(); @@ -303,17 +297,15 @@ public class MinimapCapture : IDisposable using var wallMask = BuildWallMask(hsv, playerMask); - // Colorized classified (walls + player) using var classified = new Mat(bgr.Height, bgr.Width, MatType.CV_8UC3, Scalar.Black); - classified.SetTo(new Scalar(26, 45, 61), wallMask); // brown - classified.SetTo(new Scalar(0, 165, 255), playerMask); // orange + classified.SetTo(new Scalar(26, 45, 61), wallMask); + classified.SetTo(new Scalar(0, 165, 255), playerMask); Cv2.ImWrite(Path.Combine(dir, "01-raw.png"), bgr); Cv2.ImWrite(Path.Combine(dir, "02-walls.png"), wallMask); Cv2.ImWrite(Path.Combine(dir, "03-player.png"), playerMask); Cv2.ImWrite(Path.Combine(dir, "04-classified.png"), classified); - // HSV channels var channels = Cv2.Split(hsv); Cv2.ImWrite(Path.Combine(dir, "05-hue.png"), channels[0]); Cv2.ImWrite(Path.Combine(dir, "06-sat.png"), channels[1]); @@ -326,7 +318,7 @@ public class MinimapCapture : IDisposable private Point2d FindCentroid(Mat mask) { var moments = Cv2.Moments(mask, true); - if (moments.M00 < 10) // not enough pixels + if (moments.M00 < 10) return new Point2d(0, 0); var cx = moments.M10 / moments.M00 - mask.Width / 2.0; @@ -334,14 +326,11 @@ public class MinimapCapture : IDisposable return new Point2d(cx, cy); } - /// Commit pending wall color samples (call after successful template match). public void CommitWallColors() => _colorTracker.Commit(); - - /// Reset adaptive color tracking (call on area change). public void ResetAdaptation() => _colorTracker.Reset(); public void Dispose() { - _backend.Dispose(); + _lastFrame?.Dispose(); } } diff --git a/src/Poe2Trade.Navigation/NavigationExecutor.cs b/src/Poe2Trade.Navigation/NavigationExecutor.cs index d7c3310..29fafb9 100644 --- a/src/Poe2Trade.Navigation/NavigationExecutor.cs +++ b/src/Poe2Trade.Navigation/NavigationExecutor.cs @@ -9,6 +9,7 @@ public class NavigationExecutor : IDisposable { private readonly IGameController _game; private readonly MinimapConfig _config; + private readonly FramePipeline _pipeline; private readonly MinimapCapture _capture; private readonly WorldMap _worldMap; private NavigationState _state = NavigationState.Idle; @@ -24,7 +25,12 @@ public class NavigationExecutor : IDisposable { _game = game; _config = config ?? new MinimapConfig(); - _capture = new MinimapCapture(_config); + + var backend = CreateBackend(); + _pipeline = new FramePipeline(backend); + _capture = new MinimapCapture(_config, backend); + _pipeline.AddConsumer(_capture); + _worldMap = new WorldMap(_config); _capture.ModeChanged += _ => { @@ -34,6 +40,35 @@ public class NavigationExecutor : IDisposable }; } + private static IScreenCapture CreateBackend() + { + // WGC primary → DXGI fallback → GDI last resort + try + { + var wgc = new WgcCapture(); + Log.Information("Screen capture: WGC (Windows Graphics Capture)"); + return wgc; + } + catch (Exception ex) + { + Log.Warning(ex, "WGC unavailable, trying DXGI Desktop Duplication"); + } + + try + { + var dxgi = new DesktopDuplication(); + Log.Information("Screen capture: DXGI Desktop Duplication"); + return dxgi; + } + catch (Exception ex) + { + Log.Warning(ex, "DXGI unavailable, falling back to GDI"); + } + + Log.Information("Screen capture: GDI (CopyFromScreen)"); + return new GdiCapture(); + } + private void SetState(NavigationState s) { _state = s; @@ -76,9 +111,10 @@ public class NavigationExecutor : IDisposable try { - // 1. Capture + track every frame + // 1. Capture + process via pipeline (single full-screen capture) SetState(NavigationState.Capturing); - using var frame = _capture.CaptureFrame(); + await _pipeline.ProcessOneFrame(); + var frame = _capture.LastFrame; if (frame == null) { Log.Warning("Failed to capture minimap frame"); @@ -238,15 +274,16 @@ public class NavigationExecutor : IDisposable public byte[] GetViewportSnapshot(int viewSize = 400) => _worldMap.GetViewportSnapshot(_worldMap.Position, viewSize); /// - /// Capture one frame, track position, stitch into world map. + /// Capture one frame via pipeline, track position, stitch into world map. /// Returns PNG bytes for the requested debug stage (or world map viewport by default). /// - public byte[]? ProcessFrame(MinimapDebugStage stage = MinimapDebugStage.WorldMap) + public async Task ProcessFrame(MinimapDebugStage stage = MinimapDebugStage.WorldMap) { var sw = Stopwatch.StartNew(); var captureStart = sw.Elapsed.TotalMilliseconds; - using var frame = _capture.CaptureFrame(); + await _pipeline.ProcessOneFrame(); + var frame = _capture.LastFrame; var captureMs = sw.Elapsed.TotalMilliseconds - captureStart; if (frame == null) return null; @@ -276,6 +313,7 @@ public class NavigationExecutor : IDisposable public void Dispose() { _capture.Dispose(); + _pipeline.Dispose(); _worldMap.Dispose(); } } diff --git a/src/Poe2Trade.Navigation/ScreenFrame.cs b/src/Poe2Trade.Navigation/ScreenFrame.cs new file mode 100644 index 0000000..f4d75ef --- /dev/null +++ b/src/Poe2Trade.Navigation/ScreenFrame.cs @@ -0,0 +1,37 @@ +using OpenCvSharp; +using Region = Poe2Trade.Core.Region; + +namespace Poe2Trade.Navigation; + +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; + + /// + /// Crop region and convert to BGR. Returns a new Mat owned by the caller. + /// + 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; + } + + /// + /// Read a single pixel (B, G, R, A) — useful for mode detection probes. + /// + public Vec4b PixelAt(int x, int y) => _bgraMat.At(y, x); + + public void Dispose() => _disposeAction(); +} diff --git a/src/Poe2Trade.Navigation/WgcCapture.cs b/src/Poe2Trade.Navigation/WgcCapture.cs index e8bc9e2..6eee6d4 100644 --- a/src/Poe2Trade.Navigation/WgcCapture.cs +++ b/src/Poe2Trade.Navigation/WgcCapture.cs @@ -123,45 +123,39 @@ public sealed class WgcCapture : IScreenCapture } } - public unsafe Mat? CaptureRegion(Region region) + public unsafe ScreenFrame? CaptureFrame() { - using var frame = _framePool.TryGetNextFrame(); + var frame = _framePool.TryGetNextFrame(); if (frame == null) return null; - using var srcTexture = GetTextureFromSurface(frame.Surface); - if (srcTexture == null) return null; + var srcTexture = GetTextureFromSurface(frame.Surface); + if (srcTexture == null) { frame.Dispose(); return null; } - EnsureStaging(region.Width, region.Height); + 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, - new Vortice.Mathematics.Box(region.X, region.Y, 0, - region.X + region.Width, region.Y + region.Height, 1)); + _context.CopySubresourceRegion(_staging!, 0, 0, 0, 0, srcTexture, 0); var mapped = _context.Map(_staging!, 0, MapMode.Read); - try - { - var mat = new Mat(region.Height, region.Width, MatType.CV_8UC4); - var rowBytes = region.Width * 4; - for (var row = 0; row < region.Height; row++) - { - Buffer.MemoryCopy( - (void*)(mapped.DataPointer + row * mapped.RowPitch), - (void*)mat.Ptr(row), - rowBytes, rowBytes); - } + // Zero-copy: Mat wraps mapped staging pointer, RowPitch handles GPU row padding + var mat = Mat.FromPixelData(h, w, MatType.CV_8UC4, mapped.DataPointer, (int)mapped.RowPitch); - var bgr = new Mat(); - Cv2.CvtColor(mat, bgr, ColorConversionCodes.BGRA2BGR); - mat.Dispose(); - return bgr; - } - finally + return new ScreenFrame(mat, () => { _context.Unmap(_staging!, 0); - } + srcTexture.Dispose(); + frame.Dispose(); + }); + } + + public unsafe Mat? CaptureRegion(Region region) + { + using var frame = CaptureFrame(); + if (frame == null) return null; + return frame.CropBgr(region); } private ID3D11Texture2D? GetTextureFromSurface(IDirect3DSurface surface) diff --git a/src/Poe2Trade.Ui/ViewModels/MainWindowViewModel.cs b/src/Poe2Trade.Ui/ViewModels/MainWindowViewModel.cs index 0087502..32d1e69 100644 --- a/src/Poe2Trade.Ui/ViewModels/MainWindowViewModel.cs +++ b/src/Poe2Trade.Ui/ViewModels/MainWindowViewModel.cs @@ -206,7 +206,7 @@ public partial class MainWindowViewModel : ObservableObject // Minimap display: if explore loop owns capture, just render viewport var bytes = _bot.Navigation.IsExploring ? _bot.Navigation.GetViewportSnapshot() - : _bot.Navigation.ProcessFrame(SelectedMinimapStage); + : await _bot.Navigation.ProcessFrame(SelectedMinimapStage); if (bytes != null) { var bmp = new Bitmap(new MemoryStream(bytes));