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
+79 -44
View File
@@ -3,10 +3,10 @@ package mindustry.ai;
import arc.*; import arc.*;
import arc.graphics.*; import arc.graphics.*;
import arc.graphics.g2d.*; import arc.graphics.g2d.*;
import arc.math.*;
import arc.math.geom.*; import arc.math.geom.*;
import arc.struct.*; import arc.struct.*;
import arc.util.*; import arc.util.*;
import arc.util.async.*;
import mindustry.core.*; import mindustry.core.*;
import mindustry.game.EventType.*; import mindustry.game.EventType.*;
import mindustry.gen.*; import mindustry.gen.*;
@@ -17,9 +17,9 @@ import static mindustry.Vars.*;
import static mindustry.ai.Pathfinder.*; import static mindustry.ai.Pathfinder.*;
//TODO I'm sure this class has countless problems //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 long maxUpdate = Time.millisToNanos(20);
private static final int updateFPS = 60; private static final int updateFPS = 50;
private static final int updateInterval = 1000 / updateFPS; private static final int updateInterval = 1000 / updateFPS;
public static boolean showDebug = false; public static boolean showDebug = false;
@@ -48,19 +48,15 @@ public class ControlPathfinder implements Runnable{
//increments each tile change //increments each tile change
static volatile int worldUpdateId; static volatile int worldUpdateId;
/** Current pathfinding thread */ /** Current pathfinding threads, contents may be null */
@Nullable Thread thread; @Nullable PathfindThread[] threads;
/** for unique target IDs */ /** for unique target IDs */
int lastTargetId = 1; int lastTargetId = 1;
/** handles task scheduling on the update thread. */
TaskQueue queue = new TaskQueue();
/** requests per-unit */ /** requests per-unit */
ObjectMap<Unit, PathRequest> requests = new ObjectMap<>(); ObjectMap<Unit, PathRequest> requests = new ObjectMap<>();
/** pathfinding thread access only! */
Seq<PathRequest> threadRequests = new Seq<>();
public ControlPathfinder(){ public ControlPathfinder(){
Events.on(WorldLoadEvent.class, event -> { Events.on(WorldLoadEvent.class, event -> {
stop(); stop();
wwidth = world.width(); wwidth = world.width();
@@ -81,7 +77,7 @@ public class ControlPathfinder implements Runnable{
//skipped N update -> drop it //skipped N update -> drop it
if(req.lastUpdateId <= state.updateId - 10){ if(req.lastUpdateId <= state.updateId - 10){
requests.remove(req.unit); 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()){ for(var req : requests.values()){
if(req.frontier == null) continue; if(req.frontier == null) continue;
//TODO will this even work
Draw.draw(Layer.overlayUI, () -> { Draw.draw(Layer.overlayUI, () -> {
if(req.done){ if(req.done){
int len = req.result.size; 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++){ for(int i = 0; i < len; i++){
Draw.color(Tmp.c1.set(Color.white).fromHsv(i / (float)len * 360f, 1f, 0.9f)); Draw.color(Tmp.c1.set(Color.white).fromHsv(i / (float)len * 360f, 1f, 0.9f));
int pos = req.result.items[i]; int pos = req.result.items[i];
@@ -129,6 +131,8 @@ public class ControlPathfinder implements Runnable{
/** @return whether a path is ready */ /** @return whether a path is ready */
public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out){ public boolean getPathPosition(Unit unit, int pathId, Vec2 destination, Vec2 out){
//uninitialized
if(threads == null) return false;
int pathType = unit.pathType(); int pathType = unit.pathType();
@@ -145,7 +149,9 @@ public class ControlPathfinder implements Runnable{
//check for request existence //check for request existence
if(!requests.containsKey(unit)){ 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.unit = unit;
req.pathType = pathType; req.pathType = pathType;
req.destination.set(destination); req.destination.set(destination);
@@ -153,11 +159,13 @@ public class ControlPathfinder implements Runnable{
req.lastUpdateId = state.updateId; req.lastUpdateId = state.updateId;
req.lastPos.set(unit); req.lastPos.set(unit);
req.lastWorldUpdate = worldUpdateId; req.lastWorldUpdate = worldUpdateId;
//raycast immediately when done
req.raycastTimer = 9999f;
requests.put(unit, req); requests.put(unit, req);
//add to thread so it gets processed next update //add to thread so it gets processed next update
queue.post(() -> threadRequests.add(req)); thread.queue.post(() -> thread.requests.add(req));
}else{ }else{
var req = requests.get(unit); var req = requests.get(unit);
req.lastUpdateId = state.updateId; req.lastUpdateId = state.updateId;
@@ -237,15 +245,24 @@ public class ControlPathfinder implements Runnable{
/** Starts or restarts the pathfinding thread. */ /** Starts or restarts the pathfinding thread. */
private void start(){ private void start(){
stop(); 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. */ /** Stops the pathfinding thread. */
private void stop(){ private void stop(){
if(thread != null){ if(threads != null){
thread.interrupt(); for(var thread : threads){
thread = null; thread.interrupt();
}
} }
threads = null;
requests.clear(); requests.clear();
} }
@@ -341,39 +358,58 @@ public class ControlPathfinder implements Runnable{
return cost(type, b); return cost(type, b);
} }
@Override static class PathfindThread extends Thread{
public void run(){ /** handles task scheduling on the update thread. */
while(true){ TaskQueue queue = new TaskQueue();
//stop on client, no updating /** pathfinding thread access only! */
if(net.client()) return; Seq<PathRequest> requests = new Seq<>();
try{ /** volatile for access across threads */
if(state.isPlaying()){ volatile int requestSize;
queue.run();
//total update time no longer than maxUpdate public PathfindThread(String name){
for(var req : threadRequests){ super(name);
//TODO this is flawed with many paths }
req.update(maxUpdate / requests.size);
}
}
@Override
public void run(){
while(true){
//stop on client, no updating
if(net.client()) return;
try{ try{
Thread.sleep(updateInterval); if(state.isPlaying()){
}catch(InterruptedException e){ queue.run();
//stop looping when interrupted externally requestSize = requests.size;
return;
//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. //TODO each one of these could run in its own thread.
static class PathRequest{ static class PathRequest{
final PathfindThread thread;
volatile boolean done = false; volatile boolean done = false;
volatile boolean foundEnd = false; volatile boolean foundEnd = false;
volatile Unit unit;
volatile int pathType;
volatile int lastWorldUpdate;
final Vec2 lastPos = new Vec2(); final Vec2 lastPos = new Vec2();
float stuckTimer = 0f; float stuckTimer = 0f;
@@ -381,10 +417,6 @@ public class ControlPathfinder implements Runnable{
final Vec2 destination = new Vec2(); final Vec2 destination = new Vec2();
final Vec2 lastDestination = new Vec2(); final Vec2 lastDestination = new Vec2();
volatile Unit unit;
volatile int pathType;
volatile int lastWorldUpdate;
//TODO only access on main thread?? //TODO only access on main thread??
volatile int pathIndex; volatile int pathIndex;
@@ -405,6 +437,10 @@ public class ControlPathfinder implements Runnable{
volatile int lastId, curId; volatile int lastId, curId;
public PathRequest(PathfindThread thread){
this.thread = thread;
}
void update(long maxUpdateNs){ void update(long maxUpdateNs){
if(curId != lastId){ if(curId != lastId){
clear(); clear();
@@ -508,7 +544,6 @@ public class ControlPathfinder implements Runnable{
void clear(){ void clear(){
done = false; done = false;
//TODO could be less expensive?
frontier = new PathfindQueue(20); frontier = new PathfindQueue(20);
cameFrom.clear(); cameFrom.clear();
costs.clear(); costs.clear();
+1 -1
View File
@@ -3801,7 +3801,7 @@ public class Blocks{
worldProcessor = new LogicBlock("world-processor"){{ worldProcessor = new LogicBlock("world-processor"){{
//currently incomplete, debugOnly for now //currently incomplete, debugOnly for now
requirements(Category.logic, BuildVisibility.debugOnly, with()); requirements(Category.logic, BuildVisibility.editorOnly, with());
//TODO customizable IPT //TODO customizable IPT
instructionsPerTick = 8; instructionsPerTick = 8;
@@ -122,6 +122,8 @@ public class CoreBlock extends StorageBlock{
@Override @Override
public boolean canPlaceOn(Tile tile, Team team, int rotation){ public boolean canPlaceOn(Tile tile, Team team, int rotation){
if(tile == null) return false; if(tile == null) return false;
//in the editor, you can place them anywhere for convenience
if(state.isEditor()) return true;
CoreBuild core = team.core(); CoreBuild core = team.core();
//must have all requirements //must have all requirements
if(core == null || (!state.rules.infiniteResources && !core.items.has(requirements, state.rules.buildCostMultiplier))) return false; if(core == null || (!state.rules.infiniteResources && !core.items.has(requirements, state.rules.buildCostMultiplier))) return false;