diff --git a/build.gradle b/build.gradle
index 923f49b27..55a816298 100644
--- a/build.gradle
+++ b/build.gradle
@@ -131,6 +131,7 @@ minecraft.runs.all {
// ^--------------------------------------------------------------------^
dependencies {
+ testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'
minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}"
jarJar('org.joml:joml:1.10.5') {
@@ -156,6 +157,10 @@ dependencies {
}
}
+test {
+ useJUnitPlatform()
+}
+
mixin {
add sourceSets.main, 'flywheel.refmap.json'
}
diff --git a/src/main/java/com/jozufozu/flywheel/api/backend/Engine.java b/src/main/java/com/jozufozu/flywheel/api/backend/Engine.java
index d4c266b0f..2d54ca396 100644
--- a/src/main/java/com/jozufozu/flywheel/api/backend/Engine.java
+++ b/src/main/java/com/jozufozu/flywheel/api/backend/Engine.java
@@ -5,13 +5,13 @@ import java.util.List;
import com.jozufozu.flywheel.api.event.RenderContext;
import com.jozufozu.flywheel.api.event.RenderStage;
import com.jozufozu.flywheel.api.instance.InstancerProvider;
+import com.jozufozu.flywheel.api.task.Plan;
import com.jozufozu.flywheel.api.task.TaskExecutor;
import net.minecraft.client.Camera;
import net.minecraft.core.Vec3i;
public interface Engine extends InstancerProvider {
- void beginFrame(TaskExecutor executor, RenderContext context);
void renderStage(TaskExecutor executor, RenderContext context, RenderStage stage);
@@ -28,4 +28,6 @@ public interface Engine extends InstancerProvider {
void addDebugInfo(List info);
void delete();
+
+ Plan planThisFrame(RenderContext context);
}
diff --git a/src/main/java/com/jozufozu/flywheel/api/task/Plan.java b/src/main/java/com/jozufozu/flywheel/api/task/Plan.java
new file mode 100644
index 000000000..5334c454c
--- /dev/null
+++ b/src/main/java/com/jozufozu/flywheel/api/task/Plan.java
@@ -0,0 +1,53 @@
+package com.jozufozu.flywheel.api.task;
+
+import com.jozufozu.flywheel.lib.task.BarrierPlan;
+import com.jozufozu.flywheel.lib.task.NestedPlan;
+
+public interface Plan {
+ /**
+ * Submit this plan for execution.
+ *
+ * You must call {@code onCompletion.run()} when the plan has completed execution.
+ *
+ * @param taskExecutor The executor to use for submitting tasks.
+ * @param onCompletion A callback to run when the plan has completed execution, useful for chaining plans.
+ */
+ void execute(TaskExecutor taskExecutor, Runnable onCompletion);
+
+ default void execute(TaskExecutor taskExecutor) {
+ execute(taskExecutor, () -> {
+ });
+ }
+
+ /**
+ * Create a new plan that executes this plan, then the given plan.
+ *
+ * @param plan The plan to execute after this plan.
+ * @return The composed plan.
+ */
+ default Plan then(Plan plan) {
+ // TODO: AbstractPlan?
+ return new BarrierPlan(this, plan);
+ }
+
+ /**
+ * Create a new plan that executes this plan and the given plan in parallel.
+ *
+ * @param plan The plan to execute in parallel with this plan.
+ * @return The composed plan.
+ */
+ default Plan and(Plan plan) {
+ return NestedPlan.of(this, plan);
+ }
+
+ /**
+ * If possible, create a new plan that accomplishes everything
+ * this plan does but with a simpler execution schedule.
+ *
+ * @return A simplified plan, or this.
+ */
+ default Plan maybeSimplify() {
+ // TODO: plan caching/simplification
+ return this;
+ }
+}
diff --git a/src/main/java/com/jozufozu/flywheel/api/task/TaskExecutor.java b/src/main/java/com/jozufozu/flywheel/api/task/TaskExecutor.java
index ee4b722bd..abe9dc71c 100644
--- a/src/main/java/com/jozufozu/flywheel/api/task/TaskExecutor.java
+++ b/src/main/java/com/jozufozu/flywheel/api/task/TaskExecutor.java
@@ -9,4 +9,6 @@ public interface TaskExecutor extends Executor {
void syncPoint();
int getThreadCount();
+
+ void scheduleForMainThread(Runnable runnable);
}
diff --git a/src/main/java/com/jozufozu/flywheel/backend/engine/batching/BatchingDrawTracker.java b/src/main/java/com/jozufozu/flywheel/backend/engine/batching/BatchingDrawTracker.java
index b423dcc96..aae68fd53 100644
--- a/src/main/java/com/jozufozu/flywheel/backend/engine/batching/BatchingDrawTracker.java
+++ b/src/main/java/com/jozufozu/flywheel/backend/engine/batching/BatchingDrawTracker.java
@@ -81,4 +81,9 @@ public class BatchingDrawTracker {
buffers.clear();
}
}
+
+ public boolean hasStage(RenderStage stage) {
+ return !activeBuffers.get(stage)
+ .isEmpty();
+ }
}
diff --git a/src/main/java/com/jozufozu/flywheel/backend/engine/batching/BatchingEngine.java b/src/main/java/com/jozufozu/flywheel/backend/engine/batching/BatchingEngine.java
index 5b4760d73..1a70ec8c8 100644
--- a/src/main/java/com/jozufozu/flywheel/backend/engine/batching/BatchingEngine.java
+++ b/src/main/java/com/jozufozu/flywheel/backend/engine/batching/BatchingEngine.java
@@ -1,5 +1,6 @@
package com.jozufozu.flywheel.backend.engine.batching;
+import java.util.ArrayList;
import java.util.List;
import com.jozufozu.flywheel.api.event.RenderContext;
@@ -8,12 +9,13 @@ import com.jozufozu.flywheel.api.instance.Instance;
import com.jozufozu.flywheel.api.instance.InstanceType;
import com.jozufozu.flywheel.api.instance.Instancer;
import com.jozufozu.flywheel.api.model.Model;
+import com.jozufozu.flywheel.api.task.Plan;
import com.jozufozu.flywheel.api.task.TaskExecutor;
import com.jozufozu.flywheel.backend.engine.AbstractEngine;
+import com.jozufozu.flywheel.lib.task.NestedPlan;
+import com.jozufozu.flywheel.lib.task.PlanUtil;
import com.jozufozu.flywheel.util.FlwUtil;
-import com.mojang.blaze3d.vertex.PoseStack;
-import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.world.phys.Vec3;
public class BatchingEngine extends AbstractEngine {
@@ -30,20 +32,24 @@ public class BatchingEngine extends AbstractEngine {
}
@Override
- public void beginFrame(TaskExecutor executor, RenderContext context) {
- transformManager.flush();
-
- var stack = FlwUtil.copyPoseStack(context.stack());
- Vec3 cameraPos = context.camera().getPosition();
- stack.translate(renderOrigin.getX() - cameraPos.x, renderOrigin.getY() - cameraPos.y, renderOrigin.getZ() - cameraPos.z);
-
- // TODO: async task executor barriers
- executor.syncPoint();
- submitTasks(executor, stack.last(), context.level());
+ public Plan planThisFrame(RenderContext context) {
+ return PlanUtil.of(transformManager::flush)
+ .then(planTransformers(context));
}
- private void submitTasks(TaskExecutor executor, PoseStack.Pose matrices, ClientLevel level) {
- for (var transformSetEntry : transformManager.getTransformSetsView().entrySet()) {
+ private Plan planTransformers(RenderContext context) {
+ Vec3 cameraPos = context.camera()
+ .getPosition();
+ var stack = FlwUtil.copyPoseStack(context.stack());
+ stack.translate(renderOrigin.getX() - cameraPos.x, renderOrigin.getY() - cameraPos.y, renderOrigin.getZ() - cameraPos.z);
+
+ var matrices = stack.last();
+ var level = context.level();
+
+ var plans = new ArrayList();
+
+ for (var transformSetEntry : transformManager.getTransformSetsView()
+ .entrySet()) {
var stage = transformSetEntry.getKey();
var transformSet = transformSetEntry.getValue();
@@ -66,15 +72,21 @@ public class BatchingEngine extends AbstractEngine {
int startVertex = 0;
for (var transformCall : transformCalls) {
- transformCall.submitTasks(executor, buffer, startVertex, matrices, level);
+ plans.add(transformCall.getPlan(buffer, startVertex, matrices, level));
startVertex += transformCall.getTotalVertexCount();
}
}
}
+
+ return new NestedPlan(plans);
}
@Override
public void renderStage(TaskExecutor executor, RenderContext context, RenderStage stage) {
+ if (!drawTracker.hasStage(stage)) {
+ return;
+ }
+ executor.syncPoint();
drawTracker.draw(stage);
}
diff --git a/src/main/java/com/jozufozu/flywheel/backend/engine/batching/TransformCall.java b/src/main/java/com/jozufozu/flywheel/backend/engine/batching/TransformCall.java
index ac3fa16b9..dfb2f98ab 100644
--- a/src/main/java/com/jozufozu/flywheel/backend/engine/batching/TransformCall.java
+++ b/src/main/java/com/jozufozu/flywheel/backend/engine/batching/TransformCall.java
@@ -1,13 +1,15 @@
package com.jozufozu.flywheel.backend.engine.batching;
+import java.util.ArrayList;
import java.util.List;
import com.jozufozu.flywheel.api.instance.Instance;
import com.jozufozu.flywheel.api.instance.InstanceVertexTransformer;
import com.jozufozu.flywheel.api.material.Material;
-import com.jozufozu.flywheel.api.task.TaskExecutor;
+import com.jozufozu.flywheel.api.task.Plan;
import com.jozufozu.flywheel.api.vertex.MutableVertexList;
import com.jozufozu.flywheel.api.vertex.ReusableVertexList;
+import com.jozufozu.flywheel.lib.task.SimplePlan;
import com.jozufozu.flywheel.lib.vertex.VertexTransformations;
import com.mojang.blaze3d.vertex.PoseStack;
import com.mojang.math.Matrix3f;
@@ -40,9 +42,11 @@ public class TransformCall {
instancer.update();
}
- public void submitTasks(TaskExecutor executor, DrawBuffer buffer, int startVertex, PoseStack.Pose matrices, ClientLevel level) {
+ public Plan getPlan(DrawBuffer buffer, int startVertex, PoseStack.Pose matrices, ClientLevel level) {
int instances = instancer.getInstanceCount();
+ var out = new ArrayList();
+
while (instances > 0) {
int end = instances;
instances -= 512;
@@ -52,8 +56,9 @@ public class TransformCall {
ReusableVertexList sub = buffer.slice(startVertex, vertexCount);
startVertex += vertexCount;
- executor.execute(() -> transformRange(sub, start, end, matrices, level));
+ out.add(() -> transformRange(sub, start, end, matrices, level));
}
+ return new SimplePlan(out);
}
public void transformRange(ReusableVertexList vertexList, int from, int to, PoseStack.Pose matrices, ClientLevel level) {
diff --git a/src/main/java/com/jozufozu/flywheel/backend/engine/indirect/IndirectCullingGroup.java b/src/main/java/com/jozufozu/flywheel/backend/engine/indirect/IndirectCullingGroup.java
index 7de4719e8..0b622ea7e 100644
--- a/src/main/java/com/jozufozu/flywheel/backend/engine/indirect/IndirectCullingGroup.java
+++ b/src/main/java/com/jozufozu/flywheel/backend/engine/indirect/IndirectCullingGroup.java
@@ -185,4 +185,7 @@ public class IndirectCullingGroup {
meshPool.delete();
}
+ public boolean hasStage(RenderStage stage) {
+ return drawSet.contains(stage);
+ }
}
diff --git a/src/main/java/com/jozufozu/flywheel/backend/engine/indirect/IndirectDrawManager.java b/src/main/java/com/jozufozu/flywheel/backend/engine/indirect/IndirectDrawManager.java
index 5dff01a79..832cd14e2 100644
--- a/src/main/java/com/jozufozu/flywheel/backend/engine/indirect/IndirectDrawManager.java
+++ b/src/main/java/com/jozufozu/flywheel/backend/engine/indirect/IndirectDrawManager.java
@@ -73,6 +73,15 @@ public class IndirectDrawManager {
initializedInstancers.add(instancer);
}
+ public boolean hasStage(RenderStage stage) {
+ for (var list : renderLists.values()) {
+ if (list.hasStage(stage)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private record UninitializedInstancer(IndirectInstancer> instancer, Model model, RenderStage stage) {
}
}
diff --git a/src/main/java/com/jozufozu/flywheel/backend/engine/indirect/IndirectEngine.java b/src/main/java/com/jozufozu/flywheel/backend/engine/indirect/IndirectEngine.java
index df7209354..489c465ee 100644
--- a/src/main/java/com/jozufozu/flywheel/backend/engine/indirect/IndirectEngine.java
+++ b/src/main/java/com/jozufozu/flywheel/backend/engine/indirect/IndirectEngine.java
@@ -10,10 +10,12 @@ import com.jozufozu.flywheel.api.instance.Instance;
import com.jozufozu.flywheel.api.instance.InstanceType;
import com.jozufozu.flywheel.api.instance.Instancer;
import com.jozufozu.flywheel.api.model.Model;
+import com.jozufozu.flywheel.api.task.Plan;
import com.jozufozu.flywheel.api.task.TaskExecutor;
import com.jozufozu.flywheel.backend.engine.AbstractEngine;
import com.jozufozu.flywheel.gl.GlStateTracker;
import com.jozufozu.flywheel.gl.GlTextureUnit;
+import com.jozufozu.flywheel.lib.task.PlanUtil;
import com.mojang.blaze3d.systems.RenderSystem;
import net.minecraft.client.Minecraft;
@@ -31,14 +33,24 @@ public class IndirectEngine extends AbstractEngine {
}
@Override
- public void beginFrame(TaskExecutor executor, RenderContext context) {
- try (var restoreState = GlStateTracker.getRestoreState()) {
+ public Plan planThisFrame(RenderContext context) {
+ return PlanUtil.onMainThread(this::flushDrawManager);
+ }
+
+ private void flushDrawManager() {
+ try (var state = GlStateTracker.getRestoreState()) {
drawManager.flush();
}
}
@Override
public void renderStage(TaskExecutor executor, RenderContext context, RenderStage stage) {
+ if (!drawManager.hasStage(stage)) {
+ return;
+ }
+
+ executor.syncPoint();
+
try (var restoreState = GlStateTracker.getRestoreState()) {
setup();
diff --git a/src/main/java/com/jozufozu/flywheel/backend/engine/instancing/InstancingEngine.java b/src/main/java/com/jozufozu/flywheel/backend/engine/instancing/InstancingEngine.java
index 8138aec31..038511ca8 100644
--- a/src/main/java/com/jozufozu/flywheel/backend/engine/instancing/InstancingEngine.java
+++ b/src/main/java/com/jozufozu/flywheel/backend/engine/instancing/InstancingEngine.java
@@ -11,6 +11,7 @@ import com.jozufozu.flywheel.api.instance.Instance;
import com.jozufozu.flywheel.api.instance.InstanceType;
import com.jozufozu.flywheel.api.instance.Instancer;
import com.jozufozu.flywheel.api.model.Model;
+import com.jozufozu.flywheel.api.task.Plan;
import com.jozufozu.flywheel.api.task.TaskExecutor;
import com.jozufozu.flywheel.backend.Pipelines;
import com.jozufozu.flywheel.backend.compile.FlwCompiler;
@@ -19,6 +20,7 @@ import com.jozufozu.flywheel.backend.engine.UniformBuffer;
import com.jozufozu.flywheel.gl.GlStateTracker;
import com.jozufozu.flywheel.gl.GlTextureUnit;
import com.jozufozu.flywheel.lib.material.MaterialIndices;
+import com.jozufozu.flywheel.lib.task.PlanUtil;
import com.mojang.blaze3d.systems.RenderSystem;
import net.minecraft.client.Minecraft;
@@ -38,8 +40,12 @@ public class InstancingEngine extends AbstractEngine {
}
@Override
- public void beginFrame(TaskExecutor executor, RenderContext context) {
- try (var state = GlStateTracker.getRestoreState()) {
+ public Plan planThisFrame(RenderContext context) {
+ return PlanUtil.onMainThread(this::flushDrawManager);
+ }
+
+ private void flushDrawManager() {
+ try (var restoreState = GlStateTracker.getRestoreState()) {
drawManager.flush();
}
}
@@ -52,6 +58,8 @@ public class InstancingEngine extends AbstractEngine {
return;
}
+ executor.syncPoint();
+
try (var state = GlStateTracker.getRestoreState()) {
setup();
diff --git a/src/main/java/com/jozufozu/flywheel/backend/task/ParallelTaskExecutor.java b/src/main/java/com/jozufozu/flywheel/backend/task/ParallelTaskExecutor.java
index 351b3d8b9..7e7e16d68 100644
--- a/src/main/java/com/jozufozu/flywheel/backend/task/ParallelTaskExecutor.java
+++ b/src/main/java/com/jozufozu/flywheel/backend/task/ParallelTaskExecutor.java
@@ -3,14 +3,18 @@ package com.jozufozu.flywheel.backend.task;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
+import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
+import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import com.jozufozu.flywheel.Flywheel;
import com.jozufozu.flywheel.api.task.TaskExecutor;
+import com.jozufozu.flywheel.lib.task.WaitGroup;
import com.mojang.logging.LogUtils;
import net.minecraft.util.Mth;
@@ -27,16 +31,13 @@ public class ParallelTaskExecutor implements TaskExecutor {
* If set to false, the executor will shut down.
*/
private final AtomicBoolean running = new AtomicBoolean(false);
- /**
- * Synchronized via {@link #tasksCompletedNotifier}.
- */
- private int incompleteTaskCounter = 0;
private final List threads = new ArrayList<>();
private final Deque taskQueue = new ConcurrentLinkedDeque<>();
+ private final Queue mainThreadQueue = new ConcurrentLinkedQueue<>();
private final Object taskNotifier = new Object();
- private final Object tasksCompletedNotifier = new Object();
+ private final WaitGroup waitGroup = new WaitGroup();
public ParallelTaskExecutor(String name) {
this.name = name;
@@ -99,27 +100,31 @@ public class ParallelTaskExecutor implements TaskExecutor {
threads.clear();
taskQueue.clear();
- synchronized (tasksCompletedNotifier) {
- incompleteTaskCounter = 0;
- tasksCompletedNotifier.notifyAll();
- }
+ mainThreadQueue.clear();
+ waitGroup._reset();
}
@Override
- public void execute(Runnable task) {
+ public void execute(@NotNull Runnable task) {
if (!running.get()) {
throw new IllegalStateException("Executor is stopped");
}
+ waitGroup.add();
taskQueue.add(task);
- synchronized (tasksCompletedNotifier) {
- incompleteTaskCounter++;
+ synchronized (taskNotifier) {
+ taskNotifier.notifyAll();
+ }
+ }
+
+ @Override
+ public void scheduleForMainThread(Runnable runnable) {
+ if (!running.get()) {
+ throw new IllegalStateException("Executor is stopped");
}
- synchronized (taskNotifier) {
- taskNotifier.notify();
- }
+ mainThreadQueue.add(runnable);
}
/**
@@ -128,19 +133,21 @@ public class ParallelTaskExecutor implements TaskExecutor {
@Override
public void syncPoint() {
Runnable task;
-
- // Finish everyone else's work...
- while ((task = taskQueue.pollLast()) != null) {
- processTask(task);
- }
-
- // and wait for any stragglers.
- synchronized (tasksCompletedNotifier) {
- while (incompleteTaskCounter > 0) {
- try {
- tasksCompletedNotifier.wait();
- } catch (InterruptedException e) {
- //
+ while (true) {
+ if ((task = mainThreadQueue.poll()) != null) {
+ // Prioritize main thread tasks.
+ processMainThreadTask(task);
+ } else if ((task = taskQueue.pollLast()) != null) {
+ // then work on tasks from the queue.
+ processTask(task);
+ } else {
+ // then wait for the other threads to finish.
+ waitGroup.await();
+ // at this point there will be no more tasks in the queue, but
+ // one of the worker threads may have submitted a main thread task.
+ if (mainThreadQueue.isEmpty()) {
+ // if they didn't, we're done.
+ break;
}
}
}
@@ -149,23 +156,13 @@ public class ParallelTaskExecutor implements TaskExecutor {
public void discardAndAwait() {
// Discard everyone else's work...
while (taskQueue.pollLast() != null) {
- synchronized (tasksCompletedNotifier) {
- if (--incompleteTaskCounter == 0) {
- tasksCompletedNotifier.notifyAll();
- }
- }
+ waitGroup.done();
}
- // and wait for any stragglers.
- synchronized (tasksCompletedNotifier) {
- while (incompleteTaskCounter > 0) {
- try {
- tasksCompletedNotifier.wait();
- } catch (InterruptedException e) {
- //
- }
- }
- }
+ // ...wait for any stragglers...
+ waitGroup.await();
+ // ...and clear the main thread queue.
+ mainThreadQueue.clear();
}
@Nullable
@@ -185,18 +182,21 @@ public class ParallelTaskExecutor implements TaskExecutor {
return task;
}
- // TODO: task context
private void processTask(Runnable task) {
try {
task.run();
} catch (Exception e) {
Flywheel.LOGGER.error("Error running task", e);
} finally {
- synchronized (tasksCompletedNotifier) {
- if (--incompleteTaskCounter == 0) {
- tasksCompletedNotifier.notifyAll();
- }
- }
+ waitGroup.done();
+ }
+ }
+
+ private void processMainThreadTask(Runnable task) {
+ try {
+ task.run();
+ } catch (Exception e) {
+ Flywheel.LOGGER.error("Error running main thread task", e);
}
}
@@ -212,7 +212,6 @@ public class ParallelTaskExecutor implements TaskExecutor {
}
private class WorkerThread extends Thread {
- private final AtomicBoolean running = ParallelTaskExecutor.this.running;
public WorkerThread(String name) {
super(name);
@@ -221,7 +220,7 @@ public class ParallelTaskExecutor implements TaskExecutor {
@Override
public void run() {
// Run until the executor shuts down
- while (running.get()) {
+ while (ParallelTaskExecutor.this.running.get()) {
Runnable task = getNextTask();
if (task == null) {
diff --git a/src/main/java/com/jozufozu/flywheel/backend/task/SerialTaskExecutor.java b/src/main/java/com/jozufozu/flywheel/backend/task/SerialTaskExecutor.java
index 6e273ab1b..f7730b406 100644
--- a/src/main/java/com/jozufozu/flywheel/backend/task/SerialTaskExecutor.java
+++ b/src/main/java/com/jozufozu/flywheel/backend/task/SerialTaskExecutor.java
@@ -13,6 +13,11 @@ public class SerialTaskExecutor implements TaskExecutor {
task.run();
}
+ @Override
+ public void scheduleForMainThread(Runnable runnable) {
+ runnable.run();
+ }
+
@Override
public void syncPoint() {
}
diff --git a/src/main/java/com/jozufozu/flywheel/handler/EntityWorldHandler.java b/src/main/java/com/jozufozu/flywheel/handler/EntityWorldHandler.java
index 409aa3f00..035b3b4bb 100644
--- a/src/main/java/com/jozufozu/flywheel/handler/EntityWorldHandler.java
+++ b/src/main/java/com/jozufozu/flywheel/handler/EntityWorldHandler.java
@@ -28,7 +28,7 @@ public class EntityWorldHandler {
if (FlwUtil.canUseVisualization(level)) {
VisualizedRenderDispatcher.getEntities(level)
- .remove(event.getEntity());
+ .queueRemove(event.getEntity());
}
}
}
diff --git a/src/main/java/com/jozufozu/flywheel/impl/visualization/VisualWorld.java b/src/main/java/com/jozufozu/flywheel/impl/visualization/VisualWorld.java
index ac2abf52e..6d5074082 100644
--- a/src/main/java/com/jozufozu/flywheel/impl/visualization/VisualWorld.java
+++ b/src/main/java/com/jozufozu/flywheel/impl/visualization/VisualWorld.java
@@ -8,6 +8,7 @@ import com.jozufozu.flywheel.api.backend.BackendManager;
import com.jozufozu.flywheel.api.backend.Engine;
import com.jozufozu.flywheel.api.event.RenderContext;
import com.jozufozu.flywheel.api.event.RenderStage;
+import com.jozufozu.flywheel.api.task.Plan;
import com.jozufozu.flywheel.api.visual.DynamicVisual;
import com.jozufozu.flywheel.api.visual.Effect;
import com.jozufozu.flywheel.api.visual.TickableVisual;
@@ -20,6 +21,7 @@ import com.jozufozu.flywheel.impl.visualization.manager.EffectVisualManager;
import com.jozufozu.flywheel.impl.visualization.manager.EntityVisualManager;
import com.jozufozu.flywheel.impl.visualization.manager.VisualManager;
import com.jozufozu.flywheel.lib.math.MatrixUtil;
+import com.jozufozu.flywheel.lib.task.PlanUtil;
import net.minecraft.core.Vec3i;
import net.minecraft.world.entity.Entity;
@@ -70,9 +72,13 @@ public class VisualWorld implements AutoCloseable {
*
*/
public void tick(double cameraX, double cameraY, double cameraZ) {
- blockEntities.tick(taskExecutor, cameraX, cameraY, cameraZ);
- entities.tick(taskExecutor, cameraX, cameraY, cameraZ);
- effects.tick(taskExecutor, cameraX, cameraY, cameraZ);
+ taskExecutor.syncPoint();
+
+ blockEntities.planThisTick(cameraX, cameraY, cameraZ)
+ .and(entities.planThisTick(cameraX, cameraY, cameraZ))
+ .and(effects.planThisTick(cameraX, cameraY, cameraZ))
+ .maybeSimplify()
+ .execute(taskExecutor);
}
/**
@@ -84,17 +90,17 @@ public class VisualWorld implements AutoCloseable {
*
*/
public void beginFrame(RenderContext context) {
- boolean originChanged = engine.updateRenderOrigin(context.camera());
-
- if (originChanged) {
- blockEntities.recreateAll();
- entities.recreateAll();
- effects.recreateAll();
- }
-
taskExecutor.syncPoint();
- if (!originChanged) {
+ getManagerPlan(context).then(engine.planThisFrame(context))
+ .maybeSimplify()
+ .execute(taskExecutor);
+ }
+
+ private Plan getManagerPlan(RenderContext context) {
+ if (engine.updateRenderOrigin(context.camera())) {
+ return PlanUtil.of(blockEntities::recreateAll, entities::recreateAll, effects::recreateAll);
+ } else {
Vec3i renderOrigin = engine.renderOrigin();
var cameraPos = context.camera()
.getPosition();
@@ -106,19 +112,16 @@ public class VisualWorld implements AutoCloseable {
proj.translate((float) (renderOrigin.getX() - cameraX), (float) (renderOrigin.getY() - cameraY), (float) (renderOrigin.getZ() - cameraZ));
FrustumIntersection frustum = new FrustumIntersection(proj);
- blockEntities.beginFrame(taskExecutor, cameraX, cameraY, cameraZ, frustum);
- entities.beginFrame(taskExecutor, cameraX, cameraY, cameraZ, frustum);
- effects.beginFrame(taskExecutor, cameraX, cameraY, cameraZ, frustum);
+ return blockEntities.planThisFrame(cameraX, cameraY, cameraZ, frustum)
+ .and(entities.planThisFrame(cameraX, cameraY, cameraZ, frustum))
+ .and(effects.planThisFrame(cameraX, cameraY, cameraZ, frustum));
}
-
- engine.beginFrame(taskExecutor, context);
}
/**
* Draw all visuals for the given stage.
*/
public void renderStage(RenderContext context, RenderStage stage) {
- taskExecutor.syncPoint();
engine.renderStage(taskExecutor, context, stage);
}
diff --git a/src/main/java/com/jozufozu/flywheel/impl/visualization/VisualizedRenderDispatcher.java b/src/main/java/com/jozufozu/flywheel/impl/visualization/VisualizedRenderDispatcher.java
index 262c996e1..7345fd775 100644
--- a/src/main/java/com/jozufozu/flywheel/impl/visualization/VisualizedRenderDispatcher.java
+++ b/src/main/java/com/jozufozu/flywheel/impl/visualization/VisualizedRenderDispatcher.java
@@ -161,7 +161,7 @@ public class VisualizedRenderDispatcher {
// Block entities are loaded while chunks are baked.
// Entities are loaded with the level, so when chunks are reloaded they need to be re-added.
ClientLevelExtension.getAllLoadedEntities(level)
- .forEach(world.getEntities()::add);
+ .forEach(world.getEntities()::queueAdd);
}
public static boolean tryAddBlockEntity(T blockEntity) {
diff --git a/src/main/java/com/jozufozu/flywheel/impl/visualization/manager/VisualManager.java b/src/main/java/com/jozufozu/flywheel/impl/visualization/manager/VisualManager.java
index 1febcea8c..25f04ef1f 100644
--- a/src/main/java/com/jozufozu/flywheel/impl/visualization/manager/VisualManager.java
+++ b/src/main/java/com/jozufozu/flywheel/impl/visualization/manager/VisualManager.java
@@ -1,22 +1,21 @@
package com.jozufozu.flywheel.impl.visualization.manager;
-import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.function.Consumer;
import org.joml.FrustumIntersection;
-import com.jozufozu.flywheel.api.task.TaskExecutor;
+import com.jozufozu.flywheel.api.task.Plan;
import com.jozufozu.flywheel.api.visual.DynamicVisual;
import com.jozufozu.flywheel.api.visual.TickableVisual;
-import com.jozufozu.flywheel.api.visual.Visual;
import com.jozufozu.flywheel.config.FlwConfig;
import com.jozufozu.flywheel.impl.visualization.ratelimit.BandedPrimeLimiter;
import com.jozufozu.flywheel.impl.visualization.ratelimit.DistanceUpdateLimiter;
import com.jozufozu.flywheel.impl.visualization.ratelimit.NonLimiter;
import com.jozufozu.flywheel.impl.visualization.storage.Storage;
import com.jozufozu.flywheel.impl.visualization.storage.Transaction;
+import com.jozufozu.flywheel.lib.task.RunOnAllPlan;
+import com.jozufozu.flywheel.lib.task.SimplePlan;
public abstract class VisualManager {
private final Queue> queue = new ConcurrentLinkedQueue<>();
@@ -48,14 +47,6 @@ public abstract class VisualManager {
return getStorage().getAllVisuals().size();
}
- public void add(T obj) {
- if (!getStorage().willAccept(obj)) {
- return;
- }
-
- getStorage().add(obj);
- }
-
public void queueAdd(T obj) {
if (!getStorage().willAccept(obj)) {
return;
@@ -64,33 +55,10 @@ public abstract class VisualManager {
queue.add(Transaction.add(obj));
}
- public void remove(T obj) {
- getStorage().remove(obj);
- }
-
public void queueRemove(T obj) {
queue.add(Transaction.remove(obj));
}
- /**
- * Update the visual associated with an object.
- *
- *
- * By default this is the only hook a {@link Visual} has to change its internal state. This is the lowest frequency
- * update hook {@link Visual} gets. For more frequent updates, see {@link TickableVisual} and
- * {@link DynamicVisual}.
- *
- *
- * @param obj the object whose visual will be updated.
- */
- public void update(T obj) {
- if (!getStorage().willAccept(obj)) {
- return;
- }
-
- getStorage().update(obj);
- }
-
public void queueUpdate(T obj) {
if (!getStorage().willAccept(obj)) {
return;
@@ -115,58 +83,32 @@ public abstract class VisualManager {
}
}
- /**
- * Ticks the VisualManager.
- *
- *
- * {@link TickableVisual}s get ticked.
- *
- * Queued updates are processed.
- *
- */
- public void tick(TaskExecutor executor, double cameraX, double cameraY, double cameraZ) {
- tickLimiter.tick();
- processQueue();
-
- var visuals = getStorage().getTickableVisuals();
- distributeWork(executor, visuals, visual -> tickVisual(visual, cameraX, cameraY, cameraZ));
+ public Plan planThisTick(double cameraX, double cameraY, double cameraZ) {
+ return SimplePlan.of(() -> {
+ tickLimiter.tick();
+ processQueue();
+ })
+ .then(RunOnAllPlan.of(getStorage()::getTickableVisuals, instance -> tickInstance(instance, cameraX, cameraY, cameraZ)));
}
- protected void tickVisual(TickableVisual visual, double cameraX, double cameraY, double cameraZ) {
- if (!visual.decreaseTickRateWithDistance() || tickLimiter.shouldUpdate(visual.distanceSquared(cameraX, cameraY, cameraZ))) {
- visual.tick();
+ protected void tickInstance(TickableVisual instance, double cameraX, double cameraY, double cameraZ) {
+ if (!instance.decreaseTickRateWithDistance() || tickLimiter.shouldUpdate(instance.distanceSquared(cameraX, cameraY, cameraZ))) {
+ instance.tick();
}
}
- public void beginFrame(TaskExecutor executor, double cameraX, double cameraY, double cameraZ, FrustumIntersection frustum) {
- frameLimiter.tick();
- processQueue();
-
- var visuals = getStorage().getDynamicVisuals();
- distributeWork(executor, visuals, visual -> updateVisual(visual, cameraX, cameraY, cameraZ, frustum));
+ public Plan planThisFrame(double cameraX, double cameraY, double cameraZ, FrustumIntersection frustum) {
+ return SimplePlan.of(() -> {
+ frameLimiter.tick();
+ processQueue();
+ })
+ .then(RunOnAllPlan.of(getStorage()::getDynamicVisuals, instance -> updateInstance(instance, cameraX, cameraY, cameraZ, frustum)));
}
- protected void updateVisual(DynamicVisual visual, double cameraX, double cameraY, double cameraZ, FrustumIntersection frustum) {
- if (!visual.decreaseFramerateWithDistance() || frameLimiter.shouldUpdate(visual.distanceSquared(cameraX, cameraY, cameraZ))) {
- if (visual.isVisible(frustum)) {
- visual.beginFrame();
- }
- }
- }
-
- private static void distributeWork(TaskExecutor executor, List visuals, Consumer action) {
- final int amount = visuals.size();
- final int threadCount = executor.getThreadCount();
-
- if (threadCount == 1) {
- executor.execute(() -> visuals.forEach(action));
- } else {
- final int stride = Math.max(amount / (threadCount * 2), 1);
- for (int start = 0; start < amount; start += stride) {
- int end = Math.min(start + stride, amount);
-
- var sub = visuals.subList(start, end);
- executor.execute(() -> sub.forEach(action));
+ protected void updateInstance(DynamicVisual instance, double cameraX, double cameraY, double cameraZ, FrustumIntersection frustum) {
+ if (!instance.decreaseFramerateWithDistance() || frameLimiter.shouldUpdate(instance.distanceSquared(cameraX, cameraY, cameraZ))) {
+ if (instance.isVisible(frustum)) {
+ instance.beginFrame();
}
}
}
diff --git a/src/main/java/com/jozufozu/flywheel/lib/light/LightUpdater.java b/src/main/java/com/jozufozu/flywheel/lib/light/LightUpdater.java
index 3bd003cd6..c7b0381dc 100644
--- a/src/main/java/com/jozufozu/flywheel/lib/light/LightUpdater.java
+++ b/src/main/java/com/jozufozu/flywheel/lib/light/LightUpdater.java
@@ -5,9 +5,7 @@ import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.stream.Stream;
-import com.jozufozu.flywheel.backend.task.FlwTaskExecutor;
import com.jozufozu.flywheel.lib.box.ImmutableBox;
-import com.jozufozu.flywheel.lib.task.WorkGroup;
import com.jozufozu.flywheel.util.FlwUtil;
import com.jozufozu.flywheel.util.WorldAttached;
@@ -30,6 +28,8 @@ public class LightUpdater {
private final WeakContainmentMultiMap listenersBySection = new WeakContainmentMultiMap<>();
private final Set tickingListeners = FlwUtil.createWeakHashSet();
+ private final Queue queue = new ConcurrentLinkedQueue<>();
+
public static LightUpdater get(LevelAccessor level) {
if (LightUpdated.receivesLightUpdates(level)) {
// The level is valid, add it to the map.
@@ -45,8 +45,8 @@ public class LightUpdater {
}
public void tick() {
+ processQueue();
tickSerial();
- //tickParallel();
}
private void tickSerial() {
@@ -57,27 +57,26 @@ public class LightUpdater {
}
}
- private void tickParallel() {
- Queue listeners = new ConcurrentLinkedQueue<>();
-
- WorkGroup.builder()
- .addTasks(tickingListeners.stream(), listener -> {
- if (listener.tickLightListener()) {
- listeners.add(listener);
- }
- })
- .onComplete(() -> listeners.forEach(this::addListener))
- .execute(FlwTaskExecutor.get());
- }
-
/**
* Add a listener.
*
* @param listener The object that wants to receive light update notifications.
*/
public void addListener(LightListener listener) {
- if (listener instanceof TickingLightListener)
+ queue.add(listener);
+ }
+
+ private synchronized void processQueue() {
+ LightListener listener;
+ while ((listener = queue.poll()) != null) {
+ doAdd(listener);
+ }
+ }
+
+ private void doAdd(LightListener listener) {
+ if (listener instanceof TickingLightListener) {
tickingListeners.add(((TickingLightListener) listener));
+ }
ImmutableBox box = listener.getVolume();
@@ -111,9 +110,13 @@ public class LightUpdater {
* @param pos The section position where light changed.
*/
public void onLightUpdate(LightLayer type, SectionPos pos) {
+ processQueue();
+
Set listeners = listenersBySection.get(pos.asLong());
- if (listeners == null || listeners.isEmpty()) return;
+ if (listeners == null || listeners.isEmpty()) {
+ return;
+ }
listeners.removeIf(LightListener::isInvalid);
diff --git a/src/main/java/com/jozufozu/flywheel/lib/model/Models.java b/src/main/java/com/jozufozu/flywheel/lib/model/Models.java
index fe55c0147..9899fc7d6 100644
--- a/src/main/java/com/jozufozu/flywheel/lib/model/Models.java
+++ b/src/main/java/com/jozufozu/flywheel/lib/model/Models.java
@@ -1,8 +1,8 @@
package com.jozufozu.flywheel.lib.model;
import java.util.Collection;
-import java.util.HashMap;
import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
import com.jozufozu.flywheel.api.event.ReloadRenderersEvent;
import com.jozufozu.flywheel.api.model.Model;
@@ -16,9 +16,9 @@ import net.minecraft.core.Direction;
import net.minecraft.world.level.block.state.BlockState;
public final class Models {
- private static final Map BLOCK_STATE = new HashMap<>();
- private static final Map PARTIAL = new HashMap<>();
- private static final Map, Model> PARTIAL_DIR = new HashMap<>();
+ private static final Map BLOCK_STATE = new ConcurrentHashMap<>();
+ private static final Map PARTIAL = new ConcurrentHashMap<>();
+ private static final Map, Model> PARTIAL_DIR = new ConcurrentHashMap<>();
public static Model block(BlockState state) {
return BLOCK_STATE.computeIfAbsent(state, it -> new BlockModelBuilder(it).build());
diff --git a/src/main/java/com/jozufozu/flywheel/lib/task/BarrierPlan.java b/src/main/java/com/jozufozu/flywheel/lib/task/BarrierPlan.java
new file mode 100644
index 000000000..c304a0867
--- /dev/null
+++ b/src/main/java/com/jozufozu/flywheel/lib/task/BarrierPlan.java
@@ -0,0 +1,26 @@
+package com.jozufozu.flywheel.lib.task;
+
+import com.jozufozu.flywheel.api.task.Plan;
+import com.jozufozu.flywheel.api.task.TaskExecutor;
+
+public record BarrierPlan(Plan first, Plan second) implements Plan {
+ @Override
+ public void execute(TaskExecutor taskExecutor, Runnable onCompletion) {
+ first.execute(taskExecutor, () -> second.execute(taskExecutor, onCompletion));
+ }
+
+ @Override
+ public Plan maybeSimplify() {
+ var first = this.first.maybeSimplify();
+ var second = this.second.maybeSimplify();
+
+ if (first == UnitPlan.INSTANCE) {
+ return second;
+ }
+ if (second == UnitPlan.INSTANCE) {
+ return first;
+ }
+
+ return new BarrierPlan(first, second);
+ }
+}
diff --git a/src/main/java/com/jozufozu/flywheel/lib/task/NestedPlan.java b/src/main/java/com/jozufozu/flywheel/lib/task/NestedPlan.java
new file mode 100644
index 000000000..e03bf0f8c
--- /dev/null
+++ b/src/main/java/com/jozufozu/flywheel/lib/task/NestedPlan.java
@@ -0,0 +1,103 @@
+package com.jozufozu.flywheel.lib.task;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.google.common.collect.ImmutableList;
+import com.jozufozu.flywheel.api.task.Plan;
+import com.jozufozu.flywheel.api.task.TaskExecutor;
+
+public record NestedPlan(List parallelPlans) implements Plan {
+ public static NestedPlan of(Plan... plans) {
+ return new NestedPlan(ImmutableList.copyOf(plans));
+ }
+
+ @Override
+ public void execute(TaskExecutor taskExecutor, Runnable onCompletion) {
+ if (parallelPlans.isEmpty()) {
+ onCompletion.run();
+ return;
+ }
+
+ var size = parallelPlans.size();
+
+ if (size == 1) {
+ parallelPlans.get(0)
+ .execute(taskExecutor, onCompletion);
+ return;
+ }
+
+ var wait = new Synchronizer(size, onCompletion);
+ for (Plan plan : parallelPlans) {
+ plan.execute(taskExecutor, wait::decrementAndEventuallyRun);
+ }
+ }
+
+ @Override
+ public Plan and(Plan plan) {
+ return new NestedPlan(ImmutableList.builder()
+ .addAll(parallelPlans)
+ .add(plan)
+ .build());
+ }
+
+ @Override
+ public Plan maybeSimplify() {
+ if (parallelPlans.isEmpty()) {
+ return UnitPlan.INSTANCE;
+ }
+
+ if (parallelPlans.size() == 1) {
+ return parallelPlans.get(0)
+ .maybeSimplify();
+ }
+
+ var simplifiedTasks = new ArrayList();
+ var simplifiedPlans = new ArrayList();
+
+ var toVisit = new ArrayDeque<>(parallelPlans);
+ while (!toVisit.isEmpty()) {
+ var plan = toVisit.pop()
+ .maybeSimplify();
+
+ if (plan == UnitPlan.INSTANCE) {
+ continue;
+ }
+
+ if (plan instanceof SimplePlan simplePlan) {
+ // merge all simple plans into one
+ simplifiedTasks.addAll(simplePlan.parallelTasks());
+ } else if (plan instanceof NestedPlan nestedPlan) {
+ // inline and re-visit nested plans
+ toVisit.addAll(nestedPlan.parallelPlans());
+ } else {
+ // /shrug
+ simplifiedPlans.add(plan);
+ }
+ }
+
+ if (simplifiedTasks.isEmpty() && simplifiedPlans.isEmpty()) {
+ // everything got simplified away
+ return UnitPlan.INSTANCE;
+ }
+
+ if (simplifiedTasks.isEmpty()) {
+ // no simple plan to create
+ if (simplifiedPlans.size() == 1) {
+ // we only contained one complex plan, so we can just return that
+ return simplifiedPlans.get(0);
+ }
+ return new NestedPlan(simplifiedPlans);
+ }
+
+ if (simplifiedPlans.isEmpty()) {
+ // we only contained simple plans, so we can just return one
+ return new SimplePlan(simplifiedTasks);
+ }
+
+ // we have both simple and complex plans, so we need to create a nested plan
+ simplifiedPlans.add(new SimplePlan(simplifiedTasks));
+ return new NestedPlan(simplifiedPlans);
+ }
+}
diff --git a/src/main/java/com/jozufozu/flywheel/lib/task/OnMainThreadPlan.java b/src/main/java/com/jozufozu/flywheel/lib/task/OnMainThreadPlan.java
new file mode 100644
index 000000000..9ef4993fc
--- /dev/null
+++ b/src/main/java/com/jozufozu/flywheel/lib/task/OnMainThreadPlan.java
@@ -0,0 +1,15 @@
+package com.jozufozu.flywheel.lib.task;
+
+import com.jozufozu.flywheel.api.task.Plan;
+import com.jozufozu.flywheel.api.task.TaskExecutor;
+
+public record OnMainThreadPlan(Runnable task) implements Plan {
+ @Override
+ public void execute(TaskExecutor taskExecutor, Runnable onCompletion) {
+ // TODO: detect if we're already on the render thread and just run the task directly
+ taskExecutor.scheduleForMainThread(() -> {
+ task.run();
+ onCompletion.run();
+ });
+ }
+}
diff --git a/src/main/java/com/jozufozu/flywheel/lib/task/PlanUtil.java b/src/main/java/com/jozufozu/flywheel/lib/task/PlanUtil.java
new file mode 100644
index 000000000..efa8b8280
--- /dev/null
+++ b/src/main/java/com/jozufozu/flywheel/lib/task/PlanUtil.java
@@ -0,0 +1,24 @@
+package com.jozufozu.flywheel.lib.task;
+
+import java.util.List;
+
+import com.jozufozu.flywheel.api.task.Plan;
+
+public class PlanUtil {
+
+ public static Plan of() {
+ return UnitPlan.INSTANCE;
+ }
+
+ public static Plan of(Plan... plans) {
+ return new NestedPlan(List.of(plans));
+ }
+
+ public static Plan of(Runnable... tasks) {
+ return SimplePlan.of(tasks);
+ }
+
+ public static Plan onMainThread(Runnable task) {
+ return new OnMainThreadPlan(task);
+ }
+}
diff --git a/src/main/java/com/jozufozu/flywheel/lib/task/RunOnAllPlan.java b/src/main/java/com/jozufozu/flywheel/lib/task/RunOnAllPlan.java
new file mode 100644
index 000000000..fb9123028
--- /dev/null
+++ b/src/main/java/com/jozufozu/flywheel/lib/task/RunOnAllPlan.java
@@ -0,0 +1,66 @@
+package com.jozufozu.flywheel.lib.task;
+
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import com.jozufozu.flywheel.api.task.Plan;
+import com.jozufozu.flywheel.api.task.TaskExecutor;
+
+public record RunOnAllPlan(Supplier> listSupplier, Consumer action) implements Plan {
+ public static Plan of(Supplier> iterable, Consumer forEach) {
+ return new RunOnAllPlan<>(iterable, forEach);
+ }
+
+ @Override
+ public void execute(TaskExecutor taskExecutor, Runnable onCompletion) {
+ taskExecutor.execute(() -> {
+ var list = listSupplier.get();
+ final int size = list.size();
+
+ if (size == 0) {
+ onCompletion.run();
+ } else if (size <= getChunkingThreshold()) {
+ processList(list, onCompletion);
+ } else {
+ dispatchChunks(list, taskExecutor, onCompletion);
+ }
+ });
+ }
+
+ private void dispatchChunks(List suppliedList, TaskExecutor taskExecutor, Runnable onCompletion) {
+ final int size = suppliedList.size();
+ final int chunkSize = getChunkSize(taskExecutor, size);
+
+ var synchronizer = new Synchronizer(ceilingDiv(size, chunkSize), onCompletion);
+ int remaining = size;
+
+ while (remaining > 0) {
+ int end = remaining;
+ remaining -= chunkSize;
+ int start = Math.max(remaining, 0);
+
+ var subList = suppliedList.subList(start, end);
+ taskExecutor.execute(() -> processList(subList, synchronizer::decrementAndEventuallyRun));
+ }
+ }
+
+ private static int getChunkSize(TaskExecutor taskExecutor, int totalSize) {
+ return ceilingDiv(totalSize, taskExecutor.getThreadCount() * 32);
+ }
+
+ private static int ceilingDiv(int numerator, int denominator) {
+ return (numerator + denominator - 1) / denominator;
+ }
+
+ private void processList(List suppliedList, Runnable onCompletion) {
+ for (T t : suppliedList) {
+ action.accept(t);
+ }
+ onCompletion.run();
+ }
+
+ private static int getChunkingThreshold() {
+ return 256;
+ }
+}
diff --git a/src/main/java/com/jozufozu/flywheel/lib/task/SimplePlan.java b/src/main/java/com/jozufozu/flywheel/lib/task/SimplePlan.java
new file mode 100644
index 000000000..e3c68506f
--- /dev/null
+++ b/src/main/java/com/jozufozu/flywheel/lib/task/SimplePlan.java
@@ -0,0 +1,37 @@
+package com.jozufozu.flywheel.lib.task;
+
+import java.util.List;
+
+import com.jozufozu.flywheel.api.task.Plan;
+import com.jozufozu.flywheel.api.task.TaskExecutor;
+
+public record SimplePlan(List parallelTasks) implements Plan {
+ public static Plan of(Runnable... tasks) {
+ return new SimplePlan(List.of(tasks));
+ }
+
+ @Override
+ public void execute(TaskExecutor taskExecutor, Runnable onCompletion) {
+ if (parallelTasks.isEmpty()) {
+ onCompletion.run();
+ return;
+ }
+
+ var synchronizer = new Synchronizer(parallelTasks.size(), onCompletion);
+ for (var task : parallelTasks) {
+ taskExecutor.execute(() -> {
+ task.run();
+ synchronizer.decrementAndEventuallyRun();
+ });
+ }
+ }
+
+ @Override
+ public Plan maybeSimplify() {
+ if (parallelTasks.isEmpty()) {
+ return UnitPlan.INSTANCE;
+ }
+
+ return this;
+ }
+}
diff --git a/src/main/java/com/jozufozu/flywheel/lib/task/Synchronizer.java b/src/main/java/com/jozufozu/flywheel/lib/task/Synchronizer.java
new file mode 100644
index 000000000..51c9716be
--- /dev/null
+++ b/src/main/java/com/jozufozu/flywheel/lib/task/Synchronizer.java
@@ -0,0 +1,19 @@
+package com.jozufozu.flywheel.lib.task;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class Synchronizer {
+ private final AtomicInteger countDown;
+ private final Runnable onCompletion;
+
+ public Synchronizer(int countDown, Runnable onCompletion) {
+ this.countDown = new AtomicInteger(countDown);
+ this.onCompletion = onCompletion;
+ }
+
+ public void decrementAndEventuallyRun() {
+ if (countDown.decrementAndGet() == 0) {
+ onCompletion.run();
+ }
+ }
+}
diff --git a/src/main/java/com/jozufozu/flywheel/lib/task/UnitPlan.java b/src/main/java/com/jozufozu/flywheel/lib/task/UnitPlan.java
new file mode 100644
index 000000000..8894b02dd
--- /dev/null
+++ b/src/main/java/com/jozufozu/flywheel/lib/task/UnitPlan.java
@@ -0,0 +1,16 @@
+package com.jozufozu.flywheel.lib.task;
+
+import com.jozufozu.flywheel.api.task.Plan;
+import com.jozufozu.flywheel.api.task.TaskExecutor;
+
+public class UnitPlan implements Plan {
+ public static final UnitPlan INSTANCE = new UnitPlan();
+
+ private UnitPlan() {
+ }
+
+ @Override
+ public void execute(TaskExecutor taskExecutor, Runnable onCompletion) {
+ onCompletion.run();
+ }
+}
diff --git a/src/main/java/com/jozufozu/flywheel/lib/task/WaitGroup.java b/src/main/java/com/jozufozu/flywheel/lib/task/WaitGroup.java
new file mode 100644
index 000000000..0d34ab324
--- /dev/null
+++ b/src/main/java/com/jozufozu/flywheel/lib/task/WaitGroup.java
@@ -0,0 +1,52 @@
+package com.jozufozu.flywheel.lib.task;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.slf4j.Logger;
+
+import com.jozufozu.flywheel.util.StringUtil;
+import com.mojang.logging.LogUtils;
+
+public class WaitGroup {
+ private static final Logger LOGGER = LogUtils.getLogger();
+
+ private final AtomicInteger counter = new AtomicInteger(0);
+
+ public void add() {
+ add(1);
+ }
+
+ public void add(int i) {
+ if (i == 0) {
+ return;
+ }
+
+ counter.addAndGet(i);
+ }
+
+ public void done() {
+ if (counter.decrementAndGet() < 0) {
+ throw new IllegalStateException("WaitGroup counter is negative!");
+ }
+ }
+
+ public void await() {
+ // TODO: comprehensive performance tracking for tasks
+ long start = System.nanoTime();
+ int count = 0;
+ while (counter.get() > 0) {
+ // spin in place to avoid sleeping the main thread
+ count++;
+ }
+ long end = System.nanoTime();
+ long elapsed = end - start;
+
+ if (elapsed > 1000000) { // > 1ms
+ LOGGER.debug("Waited " + StringUtil.formatTime(elapsed) + ", looped " + count + " times");
+ }
+ }
+
+ public void _reset() {
+ counter.set(0);
+ }
+}
diff --git a/src/main/java/com/jozufozu/flywheel/lib/task/WorkGroup.java b/src/main/java/com/jozufozu/flywheel/lib/task/WorkGroup.java
deleted file mode 100644
index 8de35d7b5..000000000
--- a/src/main/java/com/jozufozu/flywheel/lib/task/WorkGroup.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package com.jozufozu.flywheel.lib.task;
-
-import java.util.Iterator;
-import java.util.concurrent.Executor;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Consumer;
-import java.util.stream.Stream;
-
-import org.jetbrains.annotations.Nullable;
-
-public final class WorkGroup {
- public static void run(Iterator tasks, Executor executor) {
- tasks.forEachRemaining(executor::execute);
- }
-
- public static void run(Iterator tasks, @Nullable Runnable finalizer, Executor executor) {
- if (finalizer == null) {
- run(tasks, executor);
- return;
- }
-
- AtomicInteger incompleteTaskCounter = new AtomicInteger(0);
- tasks.forEachRemaining(task -> {
- incompleteTaskCounter.incrementAndGet();
- executor.execute(() -> {
- task.run();
- if (incompleteTaskCounter.decrementAndGet() == 0) {
- executor.execute(finalizer);
- }
- });
- });
- }
-
- public static Builder builder() {
- return new Builder();
- }
-
- public static class Builder {
- @Nullable
- private Runnable finalizer;
- private Stream tasks;
-
- public Builder() {
- }
-
- public Builder addTasks(Stream iterable, Consumer consumer) {
- return addTasks(iterable.map(it -> () -> consumer.accept(it)));
- }
-
- public Builder addTasks(Stream tasks) {
- if (this.tasks == null) {
- this.tasks = tasks;
- } else {
- this.tasks = Stream.concat(this.tasks, tasks);
- }
- return this;
- }
-
- public Builder onComplete(Runnable runnable) {
- this.finalizer = runnable;
- return this;
- }
-
- public void execute(Executor executor) {
- if (tasks == null) {
- return;
- }
-
- run(tasks.iterator(), finalizer, executor);
- }
- }
-}
diff --git a/src/main/java/com/jozufozu/flywheel/mixin/visualmanage/VisualAddMixin.java b/src/main/java/com/jozufozu/flywheel/mixin/visualmanage/VisualAddMixin.java
index 9d07dcf6b..5ed10212e 100644
--- a/src/main/java/com/jozufozu/flywheel/mixin/visualmanage/VisualAddMixin.java
+++ b/src/main/java/com/jozufozu/flywheel/mixin/visualmanage/VisualAddMixin.java
@@ -28,6 +28,6 @@ public class VisualAddMixin {
}
VisualizedRenderDispatcher.getBlockEntities(level)
- .add(blockEntity);
+ .queueAdd(blockEntity);
}
}
diff --git a/src/main/java/com/jozufozu/flywheel/mixin/visualmanage/VisualRemoveMixin.java b/src/main/java/com/jozufozu/flywheel/mixin/visualmanage/VisualRemoveMixin.java
index 0db6027b3..bd95b1c2a 100644
--- a/src/main/java/com/jozufozu/flywheel/mixin/visualmanage/VisualRemoveMixin.java
+++ b/src/main/java/com/jozufozu/flywheel/mixin/visualmanage/VisualRemoveMixin.java
@@ -26,6 +26,6 @@ public class VisualRemoveMixin {
}
VisualizedRenderDispatcher.getBlockEntities(level)
- .remove((BlockEntity) (Object) this);
+ .queueRemove((BlockEntity) (Object) this);
}
}
diff --git a/src/main/java/com/jozufozu/flywheel/mixin/visualmanage/VisualUpdateMixin.java b/src/main/java/com/jozufozu/flywheel/mixin/visualmanage/VisualUpdateMixin.java
index 9eb4cb37b..50bc9732e 100644
--- a/src/main/java/com/jozufozu/flywheel/mixin/visualmanage/VisualUpdateMixin.java
+++ b/src/main/java/com/jozufozu/flywheel/mixin/visualmanage/VisualUpdateMixin.java
@@ -35,6 +35,6 @@ public class VisualUpdateMixin {
}
VisualizedRenderDispatcher.getBlockEntities(level)
- .update(blockEntity);
+ .queueUpdate(blockEntity);
}
}
diff --git a/src/test/java/com/jozufozu/flywheel/lib/task/PlanCompositionTest.java b/src/test/java/com/jozufozu/flywheel/lib/task/PlanCompositionTest.java
new file mode 100644
index 000000000..31331c55a
--- /dev/null
+++ b/src/test/java/com/jozufozu/flywheel/lib/task/PlanCompositionTest.java
@@ -0,0 +1,20 @@
+package com.jozufozu.flywheel.lib.task;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import com.jozufozu.flywheel.api.task.Plan;
+
+public class PlanCompositionTest {
+
+ public static final Runnable NOOP = () -> {
+ };
+ public static final Plan SIMPLE = SimplePlan.of(NOOP);
+
+ @Test
+ void nestedPlanAnd() {
+ var empty = NestedPlan.of(SIMPLE);
+
+ Assertions.assertEquals(NestedPlan.of(SIMPLE, SIMPLE), empty.and(SIMPLE));
+ }
+}
diff --git a/src/test/java/com/jozufozu/flywheel/lib/task/PlanExecutionTest.java b/src/test/java/com/jozufozu/flywheel/lib/task/PlanExecutionTest.java
new file mode 100644
index 000000000..6f572dee7
--- /dev/null
+++ b/src/test/java/com/jozufozu/flywheel/lib/task/PlanExecutionTest.java
@@ -0,0 +1,192 @@
+package com.jozufozu.flywheel.lib.task;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.RepeatedTest;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import com.jozufozu.flywheel.api.task.Plan;
+import com.jozufozu.flywheel.backend.task.ParallelTaskExecutor;
+
+import it.unimi.dsi.fastutil.ints.IntArrayList;
+
+class PlanExecutionTest {
+
+ protected static final ParallelTaskExecutor EXECUTOR = new ParallelTaskExecutor("PlanTest");
+
+ @BeforeAll
+ public static void setUp() {
+ EXECUTOR.startWorkers();
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ EXECUTOR.stopWorkers();
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
+ void testSynchronizer(int countDown) {
+ var done = new AtomicBoolean(false);
+ var synchronizer = new Synchronizer(countDown, () -> done.set(true));
+
+ for (int i = 0; i < countDown - 1; i++) {
+ synchronizer.decrementAndEventuallyRun();
+ Assertions.assertFalse(done.get(), "Done early at " + i);
+ }
+
+ synchronizer.decrementAndEventuallyRun();
+ Assertions.assertTrue(done.get());
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10})
+ void simpleBarrierSequencing(int barriers) {
+ var sequence = new IntArrayList(barriers + 1);
+ var expected = new IntArrayList(barriers + 1);
+
+ var plan = SimplePlan.of(() -> sequence.add(1));
+ expected.add(1);
+
+ for (int i = 0; i < barriers; i++) {
+ final int sequenceNum = i + 2;
+ expected.add(sequenceNum);
+ plan = plan.then(SimplePlan.of(() -> sequence.add(sequenceNum)));
+ }
+
+ runAndWait(plan);
+
+ Assertions.assertEquals(expected, sequence);
+ }
+
+ @RepeatedTest(10)
+ void wideBarrierSequencing() {
+ var lock = new Object();
+ var sequence = new IntArrayList(8);
+
+ Runnable addOne = () -> {
+ synchronized (lock) {
+ sequence.add(1);
+ }
+ };
+ Runnable addTwo = () -> {
+ synchronized (lock) {
+ sequence.add(2);
+ }
+ };
+
+ var plan = SimplePlan.of(addOne, addOne, addOne, addOne)
+ .then(SimplePlan.of(addTwo, addTwo, addTwo, addTwo));
+
+ runAndWait(plan);
+
+ assertExpectedSequence(sequence, 1, 1, 1, 1, 2, 2, 2, 2);
+ }
+
+ @Test
+ void simpleNestedPlan() {
+ var sequence = new IntArrayList(2);
+ var plan = NestedPlan.of(SimplePlan.of(() -> sequence.add(1)));
+ runAndWait(plan);
+ assertExpectedSequence(sequence, 1);
+ }
+
+ @Test
+ void manyNestedPlans() {
+ var counter = new AtomicInteger(0);
+ var count4 = NestedPlan.of(SimplePlan.of(counter::incrementAndGet, counter::incrementAndGet), SimplePlan.of(counter::incrementAndGet, counter::incrementAndGet));
+
+ runAndWait(count4);
+ Assertions.assertEquals(4, counter.get());
+
+ counter.set(0);
+
+ var count8Barrier = NestedPlan.of(count4, count4);
+ runAndWait(count8Barrier);
+ Assertions.assertEquals(8, counter.get());
+ }
+
+ @Test
+ void unitPlan() {
+ var done = new AtomicBoolean(false);
+
+ UnitPlan.INSTANCE.execute(null, () -> done.set(true));
+
+ Assertions.assertTrue(done.get());
+ }
+
+ @Test
+ void emptyPlan() {
+ var done = new AtomicBoolean(false);
+
+ SimplePlan.of()
+ .execute(null, () -> done.set(true));
+ Assertions.assertTrue(done.get());
+
+ done.set(false);
+ NestedPlan.of()
+ .execute(null, () -> done.set(true));
+ Assertions.assertTrue(done.get());
+ }
+
+ @Test
+ void mainThreadPlan() {
+ var done = new AtomicBoolean(false);
+ var plan = new OnMainThreadPlan(() -> done.set(true));
+
+ plan.execute(EXECUTOR);
+
+ Assertions.assertFalse(done.get());
+
+ EXECUTOR.syncPoint();
+
+ Assertions.assertTrue(done.get());
+ }
+
+ private static void assertExpectedSequence(IntArrayList sequence, int... expected) {
+ Assertions.assertArrayEquals(expected, sequence.toIntArray());
+ }
+
+ public static void runAndWait(Plan plan) {
+ new TestBarrier(plan).runAndWait();
+ }
+
+ private static final class TestBarrier {
+ private final Plan plan;
+ private boolean done = false;
+
+ private TestBarrier(Plan plan) {
+ this.plan = plan;
+ }
+
+ public void runAndWait() {
+ plan.execute(EXECUTOR, this::doneWithPlan);
+
+ synchronized (this) {
+ // early exit in case the plan is already done for e.g. UnitPlan
+ if (done) {
+ return;
+ }
+
+ try {
+ wait();
+ } catch (InterruptedException ignored) {
+ // noop
+ }
+ }
+ }
+
+ public void doneWithPlan() {
+ synchronized (this) {
+ done = true;
+ notifyAll();
+ }
+ }
+ }
+}
diff --git a/src/test/java/com/jozufozu/flywheel/lib/task/PlanSimplificationTest.java b/src/test/java/com/jozufozu/flywheel/lib/task/PlanSimplificationTest.java
new file mode 100644
index 000000000..ec051599d
--- /dev/null
+++ b/src/test/java/com/jozufozu/flywheel/lib/task/PlanSimplificationTest.java
@@ -0,0 +1,109 @@
+package com.jozufozu.flywheel.lib.task;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import com.jozufozu.flywheel.api.task.Plan;
+
+public class PlanSimplificationTest {
+
+ public static final Runnable NOOP = () -> {
+ };
+ public static final Plan SIMPLE = SimplePlan.of(NOOP);
+
+ @Test
+ void emptyPlans() {
+ var empty = NestedPlan.of();
+ Assertions.assertEquals(empty.maybeSimplify(), UnitPlan.INSTANCE);
+
+ var simpleEmpty = SimplePlan.of();
+ Assertions.assertEquals(simpleEmpty.maybeSimplify(), UnitPlan.INSTANCE);
+ }
+
+ @Test
+ void nestedSimplePlans() {
+ var twoSimple = NestedPlan.of(SimplePlan.of(NOOP, NOOP, NOOP), SIMPLE);
+ Assertions.assertEquals(twoSimple.maybeSimplify(), SimplePlan.of(NOOP, NOOP, NOOP, NOOP));
+
+ var threeSimple = NestedPlan.of(SIMPLE, SIMPLE, SIMPLE);
+ Assertions.assertEquals(threeSimple.maybeSimplify(), SimplePlan.of(NOOP, NOOP, NOOP));
+ }
+
+ @Test
+ void oneNestedPlan() {
+ var oneSimple = NestedPlan.of(SIMPLE);
+
+ Assertions.assertEquals(oneSimple.maybeSimplify(), SIMPLE);
+
+ var mainThreadNoop = new OnMainThreadPlan(NOOP);
+ var oneMainThread = NestedPlan.of(mainThreadNoop);
+
+ Assertions.assertEquals(oneMainThread.maybeSimplify(), mainThreadNoop);
+
+ var barrier = new BarrierPlan(SIMPLE, SIMPLE);
+ var oneBarrier = NestedPlan.of(barrier);
+
+ Assertions.assertEquals(oneBarrier.maybeSimplify(), barrier);
+ }
+
+ @Test
+ void nestedNestedPlan() {
+ var outer = NestedPlan.of(SIMPLE);
+ var outermost = NestedPlan.of(outer);
+
+ Assertions.assertEquals(outermost.maybeSimplify(), SIMPLE);
+ }
+
+ @Test
+ void nestedUnitPlan() {
+ var onlyUnit = NestedPlan.of(UnitPlan.INSTANCE, UnitPlan.INSTANCE, UnitPlan.INSTANCE);
+ Assertions.assertEquals(onlyUnit.maybeSimplify(), UnitPlan.INSTANCE);
+
+ var unitAndSimple = NestedPlan.of(UnitPlan.INSTANCE, UnitPlan.INSTANCE, SIMPLE);
+ Assertions.assertEquals(unitAndSimple.maybeSimplify(), SIMPLE);
+ }
+
+ @Test
+ void complexNesting() {
+ var mainThreadNoop = new OnMainThreadPlan(NOOP);
+
+ var nested = NestedPlan.of(mainThreadNoop, SIMPLE);
+ Assertions.assertEquals(nested.maybeSimplify(), nested); // cannot simplify
+
+ var barrier = new BarrierPlan(SIMPLE, SIMPLE);
+ var complex = NestedPlan.of(barrier, nested);
+ Assertions.assertEquals(complex.maybeSimplify(), NestedPlan.of(barrier, mainThreadNoop, SIMPLE));
+ }
+
+ @Test
+ void nestedNoSimple() {
+ var mainThreadNoop = new OnMainThreadPlan(NOOP);
+ var barrier = new BarrierPlan(SIMPLE, SIMPLE);
+ var oneMainThread = NestedPlan.of(mainThreadNoop, NestedPlan.of(mainThreadNoop, barrier, barrier));
+
+ Assertions.assertEquals(oneMainThread.maybeSimplify(), NestedPlan.of(mainThreadNoop, mainThreadNoop, barrier, barrier));
+ }
+
+ @Test
+ void manyNestedButJustOneAfterSimplification() {
+ var barrier = new BarrierPlan(SIMPLE, SIMPLE);
+ var oneMainThread = NestedPlan.of(barrier, NestedPlan.of(UnitPlan.INSTANCE, UnitPlan.INSTANCE));
+
+ Assertions.assertEquals(oneMainThread.maybeSimplify(), barrier);
+ }
+
+ @Test
+ void barrierPlan() {
+ var doubleUnit = new BarrierPlan(UnitPlan.INSTANCE, UnitPlan.INSTANCE);
+ Assertions.assertEquals(doubleUnit.maybeSimplify(), UnitPlan.INSTANCE);
+
+ var simpleThenUnit = new BarrierPlan(SIMPLE, UnitPlan.INSTANCE);
+ Assertions.assertEquals(simpleThenUnit.maybeSimplify(), SIMPLE);
+
+ var unitThenSimple = new BarrierPlan(UnitPlan.INSTANCE, SIMPLE);
+ Assertions.assertEquals(unitThenSimple.maybeSimplify(), SIMPLE);
+
+ var simpleThenSimple = new BarrierPlan(SIMPLE, SIMPLE);
+ Assertions.assertEquals(simpleThenSimple.maybeSimplify(), new BarrierPlan(SIMPLE, SIMPLE));
+ }
+}
diff --git a/src/test/java/com/jozufozu/flywheel/lib/task/WaitGroupTest.java b/src/test/java/com/jozufozu/flywheel/lib/task/WaitGroupTest.java
new file mode 100644
index 000000000..779170299
--- /dev/null
+++ b/src/test/java/com/jozufozu/flywheel/lib/task/WaitGroupTest.java
@@ -0,0 +1,14 @@
+package com.jozufozu.flywheel.lib.task;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class WaitGroupTest {
+ @Test
+ public void testExtraDone() {
+ WaitGroup wg = new WaitGroup();
+ wg.add();
+ wg.done();
+ Assertions.assertThrows(IllegalStateException.class, wg::done);
+ }
+}