full capture

This commit is contained in:
Boki 2026-02-13 18:13:38 -05:00
parent d71c1d97c5
commit 6ea373f2c3
10 changed files with 291 additions and 173 deletions

View file

@ -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
{
using var srcTexture = resource!.QueryInterface<ID3D11Texture2D>();
EnsureStaging(region.Width, region.Height);
var srcTexture = resource!.QueryInterface<ID3D11Texture2D>();
var desc = srcTexture.Description;
var w = (int)desc.Width;
var h = (int)desc.Height;
EnsureStaging(w, h);
// 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));
_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);
}
var mat = Mat.FromPixelData(h, w, MatType.CV_8UC4, mapped.DataPointer, (int)mapped.RowPitch);
// BGRA → BGR
var bgr = new Mat();
Cv2.CvtColor(mat, bgr, ColorConversionCodes.BGRA2BGR);
mat.Dispose();
return bgr;
}
finally
var duplication = _duplication!;
return new ScreenFrame(mat, () =>
{
_context.Unmap(_staging!, 0);
srcTexture.Dispose();
resource.Dispose();
duplication.ReleaseFrame();
});
}
}
finally
public unsafe Mat? CaptureRegion(Region region)
{
resource?.Dispose();
_duplication!.ReleaseFrame();
}
using var frame = CaptureFrame();
if (frame == null) return null;
return frame.CropBgr(region);
}
private void EnsureStaging(int w, int h)

View file

@ -0,0 +1,37 @@
namespace Poe2Trade.Navigation;
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

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

View file

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

View file

@ -6,4 +6,5 @@ namespace Poe2Trade.Navigation;
public interface IScreenCapture : IDisposable
{
Mat? CaptureRegion(Region region);
ScreenFrame? CaptureFrame();
}

View file

@ -5,56 +5,36 @@ 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<MinimapMode>? 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()
/// <summary>
/// IFrameConsumer entry point — called by FramePipeline with the shared screen frame.
/// </summary>
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");
}
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();
}
public MinimapFrame? CaptureFrame()
{
// Auto-detect minimap mode every frame (just a 5x5 pixel check, negligible cost)
var detected = DetectMinimapMode();
_modeCheckCounter = 0;
var detected = DetectMinimapMode(screen);
if (detected != _detectedMode)
{
_detectedMode = detected;
@ -62,11 +42,25 @@ public class MinimapCapture : IDisposable
ResetAdaptation();
ModeChanged?.Invoke(_detectedMode);
}
}
using var bgr = CaptureAndNormalize(_detectedMode);
if (bgr == null || bgr.Empty())
return null;
var region = _detectedMode == MinimapMode.Overlay
? _config.OverlayRegion
: _config.CornerRegion;
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();
}
private MinimapFrame? ProcessBgr(Mat bgr)
{
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);
}
/// <summary>
/// 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.
/// </summary>
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<int>(i, 4); // CC_STAT_AREA
var area = stats.At<int>(i, 4);
if (area < minArea) continue;
using var comp = new Mat();
@ -242,10 +227,15 @@ public class MinimapCapture : IDisposable
/// <summary>
/// Capture a single frame and return the requested pipeline stage as PNG bytes.
/// Uses CaptureRegion directly (debug path, not the pipeline).
/// </summary>
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);
}
/// <summary>Commit pending wall color samples (call after successful template match).</summary>
public void CommitWallColors() => _colorTracker.Commit();
/// <summary>Reset adaptive color tracking (call on area change).</summary>
public void ResetAdaptation() => _colorTracker.Reset();
public void Dispose()
{
_backend.Dispose();
_lastFrame?.Dispose();
}
}

View file

@ -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);
/// <summary>
/// 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).
/// </summary>
public byte[]? ProcessFrame(MinimapDebugStage stage = MinimapDebugStage.WorldMap)
public async Task<byte[]?> 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();
}
}

View file

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

View file

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

View file

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