diff --git a/src/main/java/com/cleanroommc/kirino/ecs/system/exegraph/scheduler/SystemScheduler.java b/src/main/java/com/cleanroommc/kirino/ecs/system/exegraph/scheduler/SystemScheduler.java new file mode 100644 index 00000000..0f239f99 --- /dev/null +++ b/src/main/java/com/cleanroommc/kirino/ecs/system/exegraph/scheduler/SystemScheduler.java @@ -0,0 +1,138 @@ +package com.cleanroommc.kirino.ecs.system.exegraph.scheduler; + +import com.cleanroommc.kirino.ecs.component.ComponentRegistry; +import com.cleanroommc.kirino.ecs.system.CleanSystem; +import com.cleanroommc.kirino.ecs.system.exegraph.SystemExeFlowGraph; +import com.cleanroommc.kirino.engine.resource.ResourceLayout; +import com.cleanroommc.kirino.schemata.graph.Hypergraph; +import com.cleanroommc.kirino.utils.GraphUtils; +import com.google.common.base.Preconditions; +import com.google.common.graph.Graph; +import it.unimi.dsi.fastutil.PriorityQueue; +import it.unimi.dsi.fastutil.objects.Object2IntArrayMap; +import it.unimi.dsi.fastutil.objects.ObjectHeapPriorityQueue; +import org.jspecify.annotations.NonNull; + +import java.lang.reflect.Field; +import java.util.Map; +import java.util.Optional; + +import static com.cleanroommc.kirino.ecs.system.exegraph.SystemExeFlowGraph.Builder; + + +public final class SystemScheduler { + + private final Hypergraph graph; + private final ComponentRegistry componentRegistry; + private final int resourceCount; + + public SystemScheduler(ComponentRegistry componentRegistry, + ResourceLayout layout) { + Preconditions.checkNotNull(componentRegistry); + Preconditions.checkNotNull(layout); + + this.componentRegistry = componentRegistry; + + // + try { + final Class resourceLayoutClass = ResourceLayout.class; + final Field nextIDField = resourceLayoutClass.getDeclaredField("nextId"); + resourceCount = nextIDField.getInt(layout); + } catch (NoSuchFieldException | IllegalAccessException e) { + throw new RuntimeException(e); + } + // + + graph = new Hypergraph<>(); + } + + /** + * Add a system-component relation. If the system or component are not present, + * they will be added ot the hypergraph. + * @param system the system. + * @param priority the priority of the system, determines which system will run first. + * @param componentDependency the component the system depends on. + * @see Hypergraph#add(Object, Object) + */ + public void add(@NonNull CleanSystem system, int priority, @NonNull String componentDependency) { + Preconditions.checkNotNull(system); + Preconditions.checkState(priority >= 0); + Preconditions.checkNotNull(componentDependency); + Preconditions.checkArgument(componentRegistry.componentExists(componentDependency)); + + graph.add(new Edge(componentDependency), new Vertex(system, priority)); + } + + /** + * Add a system-resource relation. If the system or resource are not present, + * they will be added ot the hypergraph. + * @param system the system + * @param priority the priority of the system, determines which system will run first. + * @param resourceDependency the resource the system depends on. + * @see Hypergraph#add(Object, Object) + */ + public void add(@NonNull CleanSystem system, int priority, int resourceDependency) { + Preconditions.checkNotNull(system); + Preconditions.checkState(priority >= 0); + Preconditions.checkPositionIndex(resourceDependency, resourceCount); + + graph.add(new Edge(resourceDependency), new Vertex(system, priority)); + } + + public Builder scheduleSystemExecution( + Builder builder, String... stageNames) { + final int colors = Runtime.getRuntime().availableProcessors(); + Graph disjointed = this.graph.buildDisjointedGraph(); + Optional> colored = GraphUtils.colorGraph(disjointed, colors); + if (colored.isPresent()) { + PriorityQueue[] colorQueues = new PriorityQueue[colors]; + for (int i = 0; i < colors; i++) { + colorQueues[i] = new ObjectHeapPriorityQueue(); + } + for (Map.Entry entry : colored.get().entrySet()) { + colorQueues[entry.getValue()].enqueue(entry.getKey()); + } + for (int i = -1; i < stageNames.length; i++) { + for (PriorityQueue colorQueue : colorQueues) { + if (!colorQueue.isEmpty()) { + Vertex vertex = colorQueue.dequeue(); + String from = i != -1 ? stageNames[i] : SystemExeFlowGraph.START_NODE; + String to = i != stageNames.length - 1 ? stageNames[i + 1] : SystemExeFlowGraph.END_NODE; + builder.addTransition(vertex.system, from, to); + } + } + } + } + + return builder; + } + + // + private record Vertex(CleanSystem system, int priority) implements Comparable { + + @Override + public int compareTo(@NonNull Vertex o) { + Preconditions.checkNotNull(system); + + return priority - o.priority; + } + } + // + // + private enum EdgeType { + COMPONENT, + RESOURCE + } + + private record Edge(EdgeType type, Object id) { + + public Edge(int id) { + this(EdgeType.RESOURCE, id); + } + + public Edge(String name) { + this(EdgeType.COMPONENT, name); + } + } + // +} diff --git a/src/main/java/com/cleanroommc/kirino/schemata/graph/Hypergraph.java b/src/main/java/com/cleanroommc/kirino/schemata/graph/Hypergraph.java new file mode 100644 index 00000000..280ba20d --- /dev/null +++ b/src/main/java/com/cleanroommc/kirino/schemata/graph/Hypergraph.java @@ -0,0 +1,116 @@ +package com.cleanroommc.kirino.schemata.graph; + +import com.google.common.base.Preconditions; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.google.common.graph.*; +import it.unimi.dsi.fastutil.objects.ReferenceArraySet; +import org.jspecify.annotations.NonNull; + +import java.util.Set; + +/** + * Represents a hypergraph, a mathematical structure representing an + * association of many vertices to many edges. + * @param Vertex Type + * @param Edge Type + * @implNote Currently uses {@link Multimap Multimaps} for implementation. + * Can probably be sped up with a better data structure, this is a draft. + * This class does not implement all functions related to graph like graph degree + * or graph coloring because YAGNI. + */ +public class Hypergraph { + private final Multimap verticesPerEdge; + private final Multimap edgesPerVertex; + + /** + * Creates the hypergraph. + */ + public Hypergraph() { + verticesPerEdge = HashMultimap.create(); + edgesPerVertex = HashMultimap.create(); + } + + /** + * Adds a vertex to an edge in the hypergraph, if the edge does not exist, it is created. + * @param edge the edge + * @param vertex the vertex + */ + public void add(@NonNull E edge, @NonNull V vertex) { + Preconditions.checkNotNull(edge); + Preconditions.checkNotNull(vertex); + + edgesPerVertex.put(vertex, edge); + verticesPerEdge.put(edge, vertex); + } + + public void addVertexDependency(@NonNull V dependency, @NonNull V dependent) { + Preconditions.checkNotNull(dependency); + Preconditions.checkNotNull(dependent); + } + + /** + * Removes an association from the hypergraph, if there are no more associations between the edge/vertex they are deleted. + * @param edge the edge + * @param vertex the vertex + */ + public void remove(@NonNull E edge, @NonNull V vertex) { + Preconditions.checkNotNull(edge); + Preconditions.checkNotNull(vertex); + + edgesPerVertex.remove(vertex, edge); + verticesPerEdge.remove(edge, vertex); + } + + /** + * Gets all the vertices that share an edge with the vertex. + * @param vertex the vertex + * @return A set of all the vertices that share an edge with the vertex + */ + @NonNull + public Set getNeighbours(@NonNull V vertex) { + Preconditions.checkNotNull(vertex); + + ReferenceArraySet neighbours = new ReferenceArraySet<>(); + + for (E edge : this.edgesPerVertex.get(vertex)) { + for (V neighbour : this.verticesPerEdge.get(edge)) { + if (!neighbour.equals(vertex)) { + neighbours.add(neighbour); + } + } + } + + return neighbours; + } + + /** + * Squashed the hypergraph into a graph, strips edge metadata, then inverts it. + * The resulting graph contains all the vertices of the hypergraph, + * connected to vertices, that share no edges. + * @return Inverted squashed graph. + * @apiNote Uses classes and methods marked with {@link com.google.common.annotations.Beta @Beta} + */ + @NonNull + public Graph buildDisjointedGraph() { + ImmutableGraph.Builder builder = GraphBuilder.undirected() + .allowsSelfLoops(false) + .expectedNodeCount(verticesPerEdge.keySet().size()) + .immutable(); + + Set vertices = edgesPerVertex.keySet(); + + for (V vertex : vertices) { + builder.addNode(vertex); + Set neighbours = getNeighbours(vertex); + for (V tmp : vertices) { + if (!tmp.equals(vertex) && !neighbours.contains(tmp)) { + builder.addNode(tmp); + builder.putEdge(vertex, tmp); + } + } + } + + return builder.build(); + } +} diff --git a/src/main/java/com/cleanroommc/kirino/utils/GraphUtils.java b/src/main/java/com/cleanroommc/kirino/utils/GraphUtils.java new file mode 100644 index 00000000..72d4307b --- /dev/null +++ b/src/main/java/com/cleanroommc/kirino/utils/GraphUtils.java @@ -0,0 +1,98 @@ +package com.cleanroommc.kirino.utils; + +import com.google.common.base.Preconditions; +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.google.common.graph.Graph; +import it.unimi.dsi.fastutil.objects.Reference2IntArrayMap; +import org.jspecify.annotations.NonNull; + +import java.util.*; + +/** + * Utilities regarding graphs unavailable in {@link Graph guava graph classes}. + */ +public final class GraphUtils { + + /** + * DSatur algorithm for graph coloring. + * @param graph The {@link Graph graph} + * @param availableColors Available Colors + * @return An {@link Optional} of a {@link Map} of vertices to colors, + * if the graph is empty, then the value is empty as well. + * @param Type of graph vertices. + */ + public static Optional> colorGraph(@NonNull Graph graph, int availableColors) { // TODO: throw an error or return Optional.empty() if availableColors is lower than the chromatic number of the graph + Preconditions.checkNotNull(graph); + + if (graph.nodes().isEmpty() || graph.edges().isEmpty()) { + return Optional.empty(); + } + + record VertexInfo(int saturation, int degree, V vertex) implements Comparable> { + @Override + public int compareTo(@NonNull VertexInfo o) { + Preconditions.checkNotNull(o); + if (this.saturation != o.saturation) { + return saturation - o.saturation; + } else if (degree != o.degree) { + return degree - o.degree; + } else { + return vertex.hashCode() - o.vertex.hashCode(); + } + } + } + + BitSet usedColors = new BitSet(availableColors); + V currVertex; + int currColor; + Map colors = new Reference2IntArrayMap<>(); + Map degrees = new Reference2IntArrayMap<>(); + Multimap adjColors = HashMultimap.create(); + PriorityQueue> verticesToColor = new PriorityQueue<>(); // TODO: Replace with Fibonacci Heap + + for (V v : graph.nodes()) { + colors.put(v, -1); + degrees.put(v, graph.degree(v)); + verticesToColor.add(new VertexInfo<>(0, degrees.get(v), v)); + } + + while (!verticesToColor.isEmpty()) { + VertexInfo info = verticesToColor.poll(); + currVertex = info.vertex; + Set adj = graph.adjacentNodes(currVertex); + // Set all unavailable colors. + for (V v : adj) { + if (colors.get(v) != -1) { + usedColors.set(colors.get(v)); + } + } + // Find first availableColor + for (currColor = 0; currColor < availableColors; currColor++) { + if (!usedColors.get(currColor)) { + break; + } + } + // Reset color filter + for (V v : adj) { + if (colors.get(v) != -1) { + usedColors.set(colors.get(v), false); + } + } + colors.put(currVertex, currColor); // Set color + // Push adjacent vertices to coloring queue + for (V v : adj) { + if (colors.get(v) == -1) { + verticesToColor.remove(new VertexInfo<>(adjColors.get(v).size(), + degrees.get(v), v)); + adjColors.put(v, currColor); + degrees.compute(v, (ignored, val) -> val != null ? --val : graph.degree(v)-1); + verticesToColor.add(new VertexInfo<>(adjColors.get(v).size(), + degrees.get(v), v)); + } + } + } + + return Optional.of(colors); + } +} diff --git a/src/test/java/com/cleanroommc/test/kirino/graph/GraphColoringTest.java b/src/test/java/com/cleanroommc/test/kirino/graph/GraphColoringTest.java new file mode 100644 index 00000000..fdeac07e --- /dev/null +++ b/src/test/java/com/cleanroommc/test/kirino/graph/GraphColoringTest.java @@ -0,0 +1,44 @@ +package com.cleanroommc.test.kirino.graph; + +import com.cleanroommc.kirino.utils.GraphUtils; +import com.google.common.graph.GraphBuilder; +import com.google.common.graph.MutableGraph; +import org.junit.jupiter.api.Test; + +import java.util.Map; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GraphColoringTest { + @Test + public void testGraphColoring() { + MutableGraph graph = GraphBuilder.undirected().allowsSelfLoops(false).build(); + graph.putEdge(0, 1); + graph.putEdge(1, 2); + graph.putEdge(2, 3); + graph.putEdge(3, 4); + graph.putEdge(4, 5); + graph.putEdge(5, 6); + graph.putEdge(6, 0); + graph.putEdge(0, 2); + graph.putEdge(1, 3); + graph.putEdge(2, 4); + graph.putEdge(3, 5); + graph.putEdge(4, 6); + graph.putEdge(5, 0); + graph.putEdge(6, 1); + graph.putEdge(0, 3); + graph.putEdge(1, 4); + graph.putEdge(2, 5); + Optional> colored = GraphUtils.colorGraph(graph, 3); + assertTrue(colored.isPresent()); + Map color = colored.get(); + for (var colorEntry : color.entrySet()) { + for (Integer vertex : graph.adjacentNodes(colorEntry.getKey())) { + assertNotEquals(colorEntry.getValue(), color.get(vertex)); + } + } + } +} diff --git a/src/test/java/com/cleanroommc/test/kirino/graph/HypergraphTest.java b/src/test/java/com/cleanroommc/test/kirino/graph/HypergraphTest.java new file mode 100644 index 00000000..68bb9e38 --- /dev/null +++ b/src/test/java/com/cleanroommc/test/kirino/graph/HypergraphTest.java @@ -0,0 +1,63 @@ +package com.cleanroommc.test.kirino.graph; + +import com.cleanroommc.kirino.schemata.graph.Hypergraph; +import com.google.common.graph.Graph; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class HypergraphTest { + @Test + public void testDisjointedGraph() { + Hypergraph graph = new Hypergraph<>(); + graph.add(0,0); + graph.add(0,1); + graph.add(0,2); + graph.add(1,0); + graph.add(1,3); + graph.add(1,4); + graph.add(2,1); + graph.add(2,3); + graph.add(2,5); + Graph disjointed = graph.buildDisjointedGraph(); + assertTrue(disjointed.hasEdgeConnecting(0,5)); + assertTrue(disjointed.hasEdgeConnecting(1,4)); + assertTrue(disjointed.hasEdgeConnecting(2,3)); + assertTrue(disjointed.hasEdgeConnecting(2,4)); + assertTrue(disjointed.hasEdgeConnecting(2,5)); + assertFalse(disjointed.hasEdgeConnecting(0,1)); + assertFalse(disjointed.hasEdgeConnecting(0,2)); + assertFalse(disjointed.hasEdgeConnecting(1,2)); + assertFalse(disjointed.hasEdgeConnecting(1,3)); + assertFalse(disjointed.hasEdgeConnecting(1,5)); + assertFalse(disjointed.hasEdgeConnecting(3,4)); + assertFalse(disjointed.hasEdgeConnecting(3,5)); + } + + @Test + public void testVertexDependencies() { + Hypergraph graph = new Hypergraph<>(); + graph.add(0,0); + graph.add(1,1); + graph.add(2,2); + graph.add(3,3); + graph.add(4,4); + graph.add(5,5); + graph.add(6,6); + graph.addVertexDependency(0,1); + graph.addVertexDependency(1,2); + graph.addVertexDependency(2,3); + graph.addVertexDependency(4,5); + graph.addVertexDependency(5,6); + graph.addVertexDependency(6,3); + Graph disjointed = graph.buildDisjointedGraph(); + assertTrue(disjointed.hasEdgeConnecting(0,4)); + assertTrue(disjointed.hasEdgeConnecting(1,5)); + assertTrue(disjointed.hasEdgeConnecting(2,6)); + for (int i = 0; i <= 6; i++) { + if (i != 3) { + assertFalse(disjointed.hasEdgeConnecting(i,3)); + } + } + } +}