More pathfinding multithreading
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user