From 9de6293b1a4f35ff0339babc8b2412c9accc0ee9 Mon Sep 17 00:00:00 2001 From: Boki Date: Sun, 15 Feb 2026 18:39:55 -0500 Subject: [PATCH] async improvement --- src/Poe2Trade.Navigation/MinimapCapture.cs | 2 +- .../NavigationExecutor.cs | 103 ++++++++++++++---- src/Poe2Trade.Navigation/WorldMap.cs | 4 +- src/Poe2Trade.Screen/DesktopDuplication.cs | 26 +++-- 4 files changed, 101 insertions(+), 34 deletions(-) diff --git a/src/Poe2Trade.Navigation/MinimapCapture.cs b/src/Poe2Trade.Navigation/MinimapCapture.cs index 50cdfd2..af25b12 100644 --- a/src/Poe2Trade.Navigation/MinimapCapture.cs +++ b/src/Poe2Trade.Navigation/MinimapCapture.cs @@ -62,7 +62,7 @@ public class MinimapCapture : IFrameConsumer, IDisposable var frame = ProcessBgr(bgr); if (frame == null) return; - Log.Information("Process: mode={Mode} cropSize={W}x{H} classifiedSize={CW}x{CH} wallSize={WW}x{WH}", + Log.Debug("Process: mode={Mode} cropSize={W}x{H} classifiedSize={CW}x{CH} wallSize={WW}x{WH}", _detectedMode, bgr.Width, bgr.Height, frame.ClassifiedMat.Width, frame.ClassifiedMat.Height, frame.WallMask.Width, frame.WallMask.Height); diff --git a/src/Poe2Trade.Navigation/NavigationExecutor.cs b/src/Poe2Trade.Navigation/NavigationExecutor.cs index 644d409..34bbd12 100644 --- a/src/Poe2Trade.Navigation/NavigationExecutor.cs +++ b/src/Poe2Trade.Navigation/NavigationExecutor.cs @@ -14,11 +14,16 @@ public class NavigationExecutor : IDisposable private readonly MinimapCapture _capture; private readonly WorldMap _worldMap; private NavigationState _state = NavigationState.Idle; - private bool _stopped; + private volatile bool _stopped; private int _stuckCounter; private MapPosition? _lastPosition; + private volatile byte[]? _cachedViewport; private static readonly Random Rng = new(); + // Input loop communication (capture loop writes, input loop reads) + private double _desiredDirX, _desiredDirY; + private volatile bool _directionChanged; + public event Action? StateChanged; public NavigationState State => _state; @@ -87,11 +92,13 @@ public class NavigationExecutor : IDisposable public async Task RunExploreLoop() { _stopped = false; + _directionChanged = false; Log.Information("Starting explore loop"); + _cachedViewport = _worldMap.GetViewportSnapshot(_worldMap.Position); var lastMoveTime = 0L; - var lastClickTime = 0L; - var heldKeys = new HashSet(); // currently held WASD keys + // Input loop runs concurrently — handles WASD keys + combat clicks + var inputTask = RunInputLoop(); try { @@ -116,7 +123,11 @@ public class NavigationExecutor : IDisposable var mode = _capture.DetectedMode; var pos = _worldMap.MatchAndStitch(frame.ClassifiedMat, frame.WallMask, mode); if (_worldMap.LastMatchSucceeded) + { _capture.CommitWallColors(); + // Only re-render viewport when canvas was modified (avoids ~3ms PNG encode on dedup-skips) + _cachedViewport = _worldMap.GetViewportSnapshot(pos); + } // Stuck detection if (_lastPosition != null) @@ -130,7 +141,7 @@ public class NavigationExecutor : IDisposable } _lastPosition = pos; - // 2. Movement decisions (faster re-evaluation when stuck) + // 2. Movement decisions — non-blocking, just post direction to input loop var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); var moveInterval = _stuckCounter >= _config.StuckFrameCount ? 200 @@ -156,24 +167,14 @@ public class NavigationExecutor : IDisposable } SetState(NavigationState.Moving); - await UpdateWasdKeys(heldKeys, direction.Value.dirX, direction.Value.dirY); + // Post direction to input loop (non-blocking) + _desiredDirX = direction.Value.dirX; + _desiredDirY = direction.Value.dirY; + _directionChanged = true; if (_stuckCounter >= _config.StuckFrameCount) _stuckCounter = 0; // reset after re-routing } - - // 3. Occasional combat clicks near screen center - if (now - lastClickTime >= 1000 + Rng.Next(1000)) - { - lastClickTime = now; - var cx = 1280 + Rng.Next(-150, 150); - var cy = 720 + Rng.Next(-150, 150); - await _game.LeftClickAt(cx, cy); - await Helpers.Sleep(100 + Rng.Next(100)); - cx = 1280 + Rng.Next(-150, 150); - cy = 720 + Rng.Next(-150, 150); - await _game.RightClickAt(cx, cy); - } } catch (Exception ex) { @@ -192,9 +193,9 @@ public class NavigationExecutor : IDisposable } finally { - // Always release held keys - foreach (var key in heldKeys) - await _game.KeyUp(key); + _stopped = true; // signal input loop to exit + _cachedViewport = null; + await inputTask; // wait for input loop to release keys } if (_state != NavigationState.Completed) @@ -203,6 +204,57 @@ public class NavigationExecutor : IDisposable Log.Information("Explore loop ended"); } + /// + /// Runs concurrently with the capture loop. Owns all game input: + /// WASD key holds (from direction posted by capture loop) and periodic combat clicks. + /// + private async Task RunInputLoop() + { + var heldKeys = new HashSet(); + var nextCombatTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + 1000 + Rng.Next(1000); + + try + { + while (!_stopped) + { + // Apply direction changes from capture loop + if (_directionChanged) + { + _directionChanged = false; + var dirX = _desiredDirX; + var dirY = _desiredDirY; + await UpdateWasdKeys(heldKeys, dirX, dirY); + } + + // Combat clicks on timer + var now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + if (now >= nextCombatTime) + { + nextCombatTime = now + 1000 + Rng.Next(1000); + var cx = 1280 + Rng.Next(-150, 150); + var cy = 720 + Rng.Next(-150, 150); + await _game.LeftClickAt(cx, cy); + await Helpers.Sleep(100 + Rng.Next(100)); + cx = 1280 + Rng.Next(-150, 150); + cy = 720 + Rng.Next(-150, 150); + await _game.RightClickAt(cx, cy); + } + + await Helpers.Sleep(15); + } + } + catch (Exception ex) + { + Log.Error(ex, "Error in input loop"); + } + finally + { + // Always release held keys + foreach (var key in heldKeys) + await _game.KeyUp(key); + } + } + /// /// Convert a direction vector to WASD key holds. Releases keys no longer needed, /// presses new ones. Supports diagonal movement (two keys at once). @@ -261,7 +313,12 @@ public class NavigationExecutor : IDisposable public bool IsExploring => _state != NavigationState.Idle && _state != NavigationState.Completed && _state != NavigationState.Failed; public MapPosition Position => _worldMap.Position; public byte[] GetMapSnapshot() => _worldMap.GetMapSnapshot(); - public byte[] GetViewportSnapshot(int viewSize = 400) => _worldMap.GetViewportSnapshot(_worldMap.Position, viewSize); + public byte[] GetViewportSnapshot(int viewSize = 400) + { + var cached = _cachedViewport; + if (cached != null) return cached; + return _worldMap.GetViewportSnapshot(_worldMap.Position, viewSize); + } /// /// Capture one frame via pipeline, track position, stitch into world map. @@ -305,7 +362,7 @@ public class NavigationExecutor : IDisposable result = _capture.CaptureStage(stage); var renderMs = sw.Elapsed.TotalMilliseconds - renderStart; - Log.Information("ProcessFrame: capture={Capture:F1}ms stitch={Stitch:F1}ms render={Render:F1}ms total={Total:F1}ms", + Log.Debug("ProcessFrame: capture={Capture:F1}ms stitch={Stitch:F1}ms render={Render:F1}ms total={Total:F1}ms", captureMs, stitchMs, renderMs, sw.Elapsed.TotalMilliseconds); return result; diff --git a/src/Poe2Trade.Navigation/WorldMap.cs b/src/Poe2Trade.Navigation/WorldMap.cs index 3e5c4a8..7dcd80e 100644 --- a/src/Poe2Trade.Navigation/WorldMap.cs +++ b/src/Poe2Trade.Navigation/WorldMap.cs @@ -67,7 +67,7 @@ public class WorldMap : IDisposable var changedPixels = Cv2.CountNonZero(xor); if (changedPixels < _config.FrameChangeThreshold) { - Log.Information("Frame dedup: {Changed} changed pixels, skipping ({Ms:F1}ms)", + Log.Debug("Frame dedup: {Changed} changed pixels, skipping ({Ms:F1}ms)", changedPixels, sw.Elapsed.TotalMilliseconds); return _position; } @@ -122,7 +122,7 @@ public class WorldMap : IDisposable var posDx = _position.X - prevPos.X; var posDy = _position.Y - prevPos.Y; - Log.Information("MatchAndStitch: mode={Mode} pos=({X:F1},{Y:F1}) moved=({Dx:F1},{Dy:F1}) dedup={Dedup:F1}ms match={Match:F1}ms stitch={Stitch:F1}ms total={Total:F1}ms", + Log.Debug("MatchAndStitch: mode={Mode} pos=({X:F1},{Y:F1}) moved=({Dx:F1},{Dy:F1}) dedup={Dedup:F1}ms match={Match:F1}ms stitch={Stitch:F1}ms total={Total:F1}ms", mode, _position.X, _position.Y, posDx, posDy, dedupMs, matchMs, stitchMs, sw.Elapsed.TotalMilliseconds); return _position; } diff --git a/src/Poe2Trade.Screen/DesktopDuplication.cs b/src/Poe2Trade.Screen/DesktopDuplication.cs index 4753a2c..8f2a30b 100644 --- a/src/Poe2Trade.Screen/DesktopDuplication.cs +++ b/src/Poe2Trade.Screen/DesktopDuplication.cs @@ -83,16 +83,26 @@ public sealed class DesktopDuplication : IScreenCapture var mapped = _context.Map(_staging!, 0, MapMode.Read); - var mat = Mat.FromPixelData(h, w, MatType.CV_8UC4, mapped.DataPointer, (int)mapped.RowPitch); + // 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(); - var duplication = _duplication!; - return new ScreenFrame(mat, () => + // CPU copy from our staging texture (no DXGI dependency, ~1.5ms for 2560×1440) + var mat = new Mat(h, w, MatType.CV_8UC4); + unsafe { - _context.Unmap(_staging!, 0); - srcTexture.Dispose(); - resource.Dispose(); - duplication.ReleaseFrame(); - }); + 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)