More pathfinding multithreading

This commit is contained in:
Anuken
2022-02-09 21:14:05 -05:00
parent 4278c057b4
commit 48b1266d19
3 changed files with 82 additions and 45 deletions

View File

@@ -3,10 +3,10 @@ package mindustry.ai;
import arc.*;
import arc.graphics.*;
import arc.graphics.g2d.*;
import arc.math.*;
import arc.math.geom.*;
import arc.struct.*;
import arc.util.*;
import arc.util.async.*;
import mindustry.core.*;
import mindustry.game.EventType.*;
import mindustry.gen.*;
@@ -17,9 +17,9 @@ import static mindustry.Vars.*;
import static mindustry.ai.Pathfinder.*;
//TODO I'm sure this class has countless problems
public class ControlPathfinder implements Runnable{
public class ControlPathfinder{
private static final long maxUpdate = Time.millisToNanos(20);
private static final int updateFPS = 60;
private static final int updateFPS = 50;
private static final int updateInterval = 1000 / updateFPS;
public static boolean showDebug = false;
@@ -48,19 +48,15 @@ public class ControlPathfinder implements Runnable{
//increments each tile change
static volatile int worldUpdateId;
/** Current pathfinding thread */
@Nullable Thread thread;
/** Current pathfinding threads, contents may be null */
@Nullable PathfindThread[] threads;
/** for unique target IDs */
int lastTargetId = 1;
/** handles task scheduling on the update thread. */
TaskQueue queue = new TaskQueue();
/** requests per-unit */
ObjectMap<Unit, PathRequest> requests = new ObjectMap<>();
/** pathfinding thread access only! */
Seq<PathRequest> threadRequests = new Seq<>();
public ControlPathfinder(){
Events.on(WorldLoadEvent.class, event -> {
stop();
wwidth = world.width();
@@ -81,7 +77,7 @@ public class ControlPathfinder implements Runnable{
//skipped N update -> drop it
if(req.lastUpdateId <= state.updateId - 10){
requests.remove(req.unit);
queue.post(() -> threadRequests.remove(req));
req.thread.queue.post(() -> req.thread.requests.remove(req));
}
}
});
@@ -91,10 +87,16 @@ public class ControlPathfinder implements Runnable{
for(var req : requests.values()){
if(req.frontier == null) continue;
//TODO will this even work
Draw.draw(Layer.overlayUI, () -> {
if(req.done){
int len = req.result.size;
int rp = req.rayPathIndex;
if(rp < len && rp >= 0){
Draw.color(Color.royal);
Tile tile = tile(req.result.items[rp]);
Lines.line(req.unit.x, req.unit.y, tile.worldx(), tile.worldy());
}
for(int i = 0; i < len; i++){
Draw.color(Tmp.c1.set(Color.white).fromHsv(i / (float)len * 360f, 1f, 0.9f));
int pos = req.result.items[i];
@@ -129,6 +131,8 @@ public class ControlPathfinder implements Runnable{
/** @return whether a path is ready */
public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out){
//uninitialized
if(threads == null) return false;
int pathType = unit.pathType();
@@ -145,7 +149,9 @@ public class ControlPathfinder implements Runnable{
//check for request existence
if(!requests.containsKey(unit)){
var req = new PathRequest();
PathfindThread thread = Structs.findMin(threads, t -> t.requestSize);
var req = new PathRequest(thread);
req.unit = unit;
req.pathType = pathType;
req.destination.set(destination);
@@ -153,11 +159,13 @@ public class ControlPathfinder implements Runnable{
req.lastUpdateId = state.updateId;
req.lastPos.set(unit);
req.lastWorldUpdate = worldUpdateId;
//raycast immediately when done
req.raycastTimer = 9999f;
requests.put(unit, req);
//add to thread so it gets processed next update
queue.post(() -> threadRequests.add(req));
thread.queue.post(() -> thread.requests.add(req));
}else{
var req = requests.get(unit);
req.lastUpdateId = state.updateId;
@@ -237,15 +245,24 @@ public class ControlPathfinder implements Runnable{
/** Starts or restarts the pathfinding thread. */
private void start(){
stop();
thread = Threads.daemon("ControlPathfinder", this);
//TODO currently capped at 6 threads, might be a good idea to make it more?
threads = new PathfindThread[Mathf.clamp(Runtime.getRuntime().availableProcessors() - 2, 1, 6)];
for(int i = 0; i < threads.length; i ++){
threads[i] = new PathfindThread("ControlPathfindThread-" + i);
threads[i].setDaemon(true);
threads[i].start();
}
}
/** Stops the pathfinding thread. */
private void stop(){
if(thread != null){
thread.interrupt();
thread = null;
if(threads != null){
for(var thread : threads){
thread.interrupt();
}
}
threads = null;
requests.clear();
}
@@ -341,39 +358,58 @@ public class ControlPathfinder implements Runnable{
return cost(type, b);
}
@Override
public void run(){
while(true){
//stop on client, no updating
if(net.client()) return;
try{
if(state.isPlaying()){
queue.run();
static class PathfindThread extends Thread{
/** handles task scheduling on the update thread. */
TaskQueue queue = new TaskQueue();
/** pathfinding thread access only! */
Seq<PathRequest> requests = new Seq<>();
/** volatile for access across threads */
volatile int requestSize;
//total update time no longer than maxUpdate
for(var req : threadRequests){
//TODO this is flawed with many paths
req.update(maxUpdate / requests.size);
}
}
public PathfindThread(String name){
super(name);
}
@Override
public void run(){
while(true){
//stop on client, no updating
if(net.client()) return;
try{
Thread.sleep(updateInterval);
}catch(InterruptedException e){
//stop looping when interrupted externally
return;
if(state.isPlaying()){
queue.run();
requestSize = requests.size;
//total update time no longer than maxUpdate
for(var req : requests){
//TODO this is flawed with many paths
req.update(maxUpdate / requests.size);
}
}
try{
Thread.sleep(updateInterval);
}catch(InterruptedException e){
//stop looping when interrupted externally
return;
}
}catch(Throwable e){
//do not crash the pathfinding thread
Log.err(e);
}
}catch(Throwable e){
//do not crash the pathfinding thread
Log.err(e);
}
}
}
//TODO each one of these could run in its own thread.
static class PathRequest{
final PathfindThread thread;
volatile boolean done = false;
volatile boolean foundEnd = false;
volatile Unit unit;
volatile int pathType;
volatile int lastWorldUpdate;
final Vec2 lastPos = new Vec2();
float stuckTimer = 0f;
@@ -381,10 +417,6 @@ public class ControlPathfinder implements Runnable{
final Vec2 destination = new Vec2();
final Vec2 lastDestination = new Vec2();
volatile Unit unit;
volatile int pathType;
volatile int lastWorldUpdate;
//TODO only access on main thread??
volatile int pathIndex;
@@ -405,6 +437,10 @@ public class ControlPathfinder implements Runnable{
volatile int lastId, curId;
public PathRequest(PathfindThread thread){
this.thread = thread;
}
void update(long maxUpdateNs){
if(curId != lastId){
clear();
@@ -508,7 +544,6 @@ public class ControlPathfinder implements Runnable{
void clear(){
done = false;
//TODO could be less expensive?
frontier = new PathfindQueue(20);
cameFrom.clear();
costs.clear();

View File

@@ -3801,7 +3801,7 @@ public class Blocks{
worldProcessor = new LogicBlock("world-processor"){{
//currently incomplete, debugOnly for now
requirements(Category.logic, BuildVisibility.debugOnly, with());
requirements(Category.logic, BuildVisibility.editorOnly, with());
//TODO customizable IPT
instructionsPerTick = 8;

View File

@@ -122,6 +122,8 @@ public class CoreBlock extends StorageBlock{
@Override
public boolean canPlaceOn(Tile tile, Team team, int rotation){
if(tile == null) return false;
//in the editor, you can place them anywhere for convenience
if(state.isEditor()) return true;
CoreBuild core = team.core();
//must have all requirements
if(core == null || (!state.rules.infiniteResources && !core.items.has(requirements, state.rules.buildCostMultiplier))) return false;