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

View file

@ -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<NavigationState>? 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<int>(); // 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");
}
/// <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>
/// 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);
}
/// <summary>
/// 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;

View file

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

View file

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