async improvement

This commit is contained in:
Boki 2026-02-15 18:39:55 -05:00
parent 3fe7c0b37d
commit 9de6293b1a
4 changed files with 101 additions and 34 deletions

View file

@ -62,7 +62,7 @@ public class MinimapCapture : IFrameConsumer, IDisposable
var frame = ProcessBgr(bgr); var frame = ProcessBgr(bgr);
if (frame == null) return; 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, _detectedMode, bgr.Width, bgr.Height,
frame.ClassifiedMat.Width, frame.ClassifiedMat.Height, frame.ClassifiedMat.Width, frame.ClassifiedMat.Height,
frame.WallMask.Width, frame.WallMask.Height); frame.WallMask.Width, frame.WallMask.Height);

View file

@ -14,11 +14,16 @@ public class NavigationExecutor : IDisposable
private readonly MinimapCapture _capture; private readonly MinimapCapture _capture;
private readonly WorldMap _worldMap; private readonly WorldMap _worldMap;
private NavigationState _state = NavigationState.Idle; private NavigationState _state = NavigationState.Idle;
private bool _stopped; private volatile bool _stopped;
private int _stuckCounter; private int _stuckCounter;
private MapPosition? _lastPosition; private MapPosition? _lastPosition;
private volatile byte[]? _cachedViewport;
private static readonly Random Rng = new(); 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<NavigationState>? StateChanged; public event Action<NavigationState>? StateChanged;
public NavigationState State => _state; public NavigationState State => _state;
@ -87,11 +92,13 @@ public class NavigationExecutor : IDisposable
public async Task RunExploreLoop() public async Task RunExploreLoop()
{ {
_stopped = false; _stopped = false;
_directionChanged = false;
Log.Information("Starting explore loop"); Log.Information("Starting explore loop");
_cachedViewport = _worldMap.GetViewportSnapshot(_worldMap.Position);
var lastMoveTime = 0L; var lastMoveTime = 0L;
var lastClickTime = 0L; // Input loop runs concurrently — handles WASD keys + combat clicks
var heldKeys = new HashSet<int>(); // currently held WASD keys var inputTask = RunInputLoop();
try try
{ {
@ -116,7 +123,11 @@ public class NavigationExecutor : IDisposable
var mode = _capture.DetectedMode; var mode = _capture.DetectedMode;
var pos = _worldMap.MatchAndStitch(frame.ClassifiedMat, frame.WallMask, mode); var pos = _worldMap.MatchAndStitch(frame.ClassifiedMat, frame.WallMask, mode);
if (_worldMap.LastMatchSucceeded) if (_worldMap.LastMatchSucceeded)
{
_capture.CommitWallColors(); _capture.CommitWallColors();
// Only re-render viewport when canvas was modified (avoids ~3ms PNG encode on dedup-skips)
_cachedViewport = _worldMap.GetViewportSnapshot(pos);
}
// Stuck detection // Stuck detection
if (_lastPosition != null) if (_lastPosition != null)
@ -130,7 +141,7 @@ public class NavigationExecutor : IDisposable
} }
_lastPosition = pos; _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 now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var moveInterval = _stuckCounter >= _config.StuckFrameCount var moveInterval = _stuckCounter >= _config.StuckFrameCount
? 200 ? 200
@ -156,24 +167,14 @@ public class NavigationExecutor : IDisposable
} }
SetState(NavigationState.Moving); 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) if (_stuckCounter >= _config.StuckFrameCount)
_stuckCounter = 0; // reset after re-routing _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) catch (Exception ex)
{ {
@ -192,9 +193,9 @@ public class NavigationExecutor : IDisposable
} }
finally finally
{ {
// Always release held keys _stopped = true; // signal input loop to exit
foreach (var key in heldKeys) _cachedViewport = null;
await _game.KeyUp(key); await inputTask; // wait for input loop to release keys
} }
if (_state != NavigationState.Completed) if (_state != NavigationState.Completed)
@ -203,6 +204,57 @@ public class NavigationExecutor : IDisposable
Log.Information("Explore loop ended"); Log.Information("Explore loop ended");
} }
/// <summary>
/// Runs concurrently with the capture loop. Owns all game input:
/// WASD key holds (from direction posted by capture loop) and periodic combat clicks.
/// </summary>
private async Task RunInputLoop()
{
var heldKeys = new HashSet<int>();
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);
}
}
/// <summary> /// <summary>
/// Convert a direction vector to WASD key holds. Releases keys no longer needed, /// Convert a direction vector to WASD key holds. Releases keys no longer needed,
/// presses new ones. Supports diagonal movement (two keys at once). /// 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 bool IsExploring => _state != NavigationState.Idle && _state != NavigationState.Completed && _state != NavigationState.Failed;
public MapPosition Position => _worldMap.Position; public MapPosition Position => _worldMap.Position;
public byte[] GetMapSnapshot() => _worldMap.GetMapSnapshot(); 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);
}
/// <summary> /// <summary>
/// Capture one frame via pipeline, track position, stitch into world map. /// Capture one frame via pipeline, track position, stitch into world map.
@ -305,7 +362,7 @@ public class NavigationExecutor : IDisposable
result = _capture.CaptureStage(stage); result = _capture.CaptureStage(stage);
var renderMs = sw.Elapsed.TotalMilliseconds - renderStart; 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); captureMs, stitchMs, renderMs, sw.Elapsed.TotalMilliseconds);
return result; return result;

View file

@ -67,7 +67,7 @@ public class WorldMap : IDisposable
var changedPixels = Cv2.CountNonZero(xor); var changedPixels = Cv2.CountNonZero(xor);
if (changedPixels < _config.FrameChangeThreshold) 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); changedPixels, sw.Elapsed.TotalMilliseconds);
return _position; return _position;
} }
@ -122,7 +122,7 @@ public class WorldMap : IDisposable
var posDx = _position.X - prevPos.X; var posDx = _position.X - prevPos.X;
var posDy = _position.Y - prevPos.Y; 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); mode, _position.X, _position.Y, posDx, posDy, dedupMs, matchMs, stitchMs, sw.Elapsed.TotalMilliseconds);
return _position; return _position;
} }

View file

@ -83,16 +83,26 @@ public sealed class DesktopDuplication : IScreenCapture
var mapped = _context.Map(_staging!, 0, MapMode.Read); 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!; // CPU copy from our staging texture (no DXGI dependency, ~1.5ms for 2560×1440)
return new ScreenFrame(mat, () => var mat = new Mat(h, w, MatType.CV_8UC4);
unsafe
{ {
_context.Unmap(_staging!, 0); var src = (byte*)mapped.DataPointer;
srcTexture.Dispose(); var dst = (byte*)mat.Data;
resource.Dispose(); var rowBytes = w * 4;
duplication.ReleaseFrame(); 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) public unsafe Mat? CaptureRegion(Region region)