mirror of
https://github.com/Jozufozu/Flywheel.git
synced 2025-01-22 10:57:55 +01:00
Merge branch '1.18/plan' into 1.18/next
# Conflicts: # src/main/java/com/jozufozu/flywheel/api/backend/Engine.java # src/main/java/com/jozufozu/flywheel/backend/engine/batching/BatchingEngine.java # src/main/java/com/jozufozu/flywheel/backend/engine/batching/TransformCall.java # src/main/java/com/jozufozu/flywheel/backend/engine/indirect/IndirectEngine.java # src/main/java/com/jozufozu/flywheel/backend/engine/instancing/InstancingEngine.java # src/main/java/com/jozufozu/flywheel/handler/EntityWorldHandler.java # src/main/java/com/jozufozu/flywheel/impl/instancing/InstancedRenderDispatcher.java # src/main/java/com/jozufozu/flywheel/impl/instancing/manager/InstanceManager.java # src/main/java/com/jozufozu/flywheel/impl/instancing/storage/AbstractStorage.java # src/main/java/com/jozufozu/flywheel/impl/visualization/VisualWorld.java # src/main/java/com/jozufozu/flywheel/mixin/instancemanage/InstanceRemoveMixin.java # src/main/java/com/jozufozu/flywheel/mixin/visualmanage/VisualAddMixin.java # src/main/java/com/jozufozu/flywheel/mixin/visualmanage/VisualUpdateMixin.java
This commit is contained in:
commit
cb74fa4603
36 changed files with 960 additions and 271 deletions
|
@ -131,6 +131,7 @@ minecraft.runs.all {
|
||||||
// ^--------------------------------------------------------------------^
|
// ^--------------------------------------------------------------------^
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
testImplementation 'org.junit.jupiter:junit-jupiter:5.8.1'
|
||||||
minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}"
|
minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}"
|
||||||
|
|
||||||
jarJar('org.joml:joml:1.10.5') {
|
jarJar('org.joml:joml:1.10.5') {
|
||||||
|
@ -156,6 +157,10 @@ dependencies {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
mixin {
|
mixin {
|
||||||
add sourceSets.main, 'flywheel.refmap.json'
|
add sourceSets.main, 'flywheel.refmap.json'
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,13 +5,13 @@ import java.util.List;
|
||||||
import com.jozufozu.flywheel.api.event.RenderContext;
|
import com.jozufozu.flywheel.api.event.RenderContext;
|
||||||
import com.jozufozu.flywheel.api.event.RenderStage;
|
import com.jozufozu.flywheel.api.event.RenderStage;
|
||||||
import com.jozufozu.flywheel.api.instance.InstancerProvider;
|
import com.jozufozu.flywheel.api.instance.InstancerProvider;
|
||||||
|
import com.jozufozu.flywheel.api.task.Plan;
|
||||||
import com.jozufozu.flywheel.api.task.TaskExecutor;
|
import com.jozufozu.flywheel.api.task.TaskExecutor;
|
||||||
|
|
||||||
import net.minecraft.client.Camera;
|
import net.minecraft.client.Camera;
|
||||||
import net.minecraft.core.Vec3i;
|
import net.minecraft.core.Vec3i;
|
||||||
|
|
||||||
public interface Engine extends InstancerProvider {
|
public interface Engine extends InstancerProvider {
|
||||||
void beginFrame(TaskExecutor executor, RenderContext context);
|
|
||||||
|
|
||||||
void renderStage(TaskExecutor executor, RenderContext context, RenderStage stage);
|
void renderStage(TaskExecutor executor, RenderContext context, RenderStage stage);
|
||||||
|
|
||||||
|
@ -28,4 +28,6 @@ public interface Engine extends InstancerProvider {
|
||||||
void addDebugInfo(List<String> info);
|
void addDebugInfo(List<String> info);
|
||||||
|
|
||||||
void delete();
|
void delete();
|
||||||
|
|
||||||
|
Plan planThisFrame(RenderContext context);
|
||||||
}
|
}
|
||||||
|
|
53
src/main/java/com/jozufozu/flywheel/api/task/Plan.java
Normal file
53
src/main/java/com/jozufozu/flywheel/api/task/Plan.java
Normal file
|
@ -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.
|
||||||
|
* <p>
|
||||||
|
* You <em>must</em> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,4 +9,6 @@ public interface TaskExecutor extends Executor {
|
||||||
void syncPoint();
|
void syncPoint();
|
||||||
|
|
||||||
int getThreadCount();
|
int getThreadCount();
|
||||||
|
|
||||||
|
void scheduleForMainThread(Runnable runnable);
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,4 +81,9 @@ public class BatchingDrawTracker {
|
||||||
buffers.clear();
|
buffers.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasStage(RenderStage stage) {
|
||||||
|
return !activeBuffers.get(stage)
|
||||||
|
.isEmpty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package com.jozufozu.flywheel.backend.engine.batching;
|
package com.jozufozu.flywheel.backend.engine.batching;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import com.jozufozu.flywheel.api.event.RenderContext;
|
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.InstanceType;
|
||||||
import com.jozufozu.flywheel.api.instance.Instancer;
|
import com.jozufozu.flywheel.api.instance.Instancer;
|
||||||
import com.jozufozu.flywheel.api.model.Model;
|
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.api.task.TaskExecutor;
|
||||||
import com.jozufozu.flywheel.backend.engine.AbstractEngine;
|
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.jozufozu.flywheel.util.FlwUtil;
|
||||||
import com.mojang.blaze3d.vertex.PoseStack;
|
|
||||||
|
|
||||||
import net.minecraft.client.multiplayer.ClientLevel;
|
|
||||||
import net.minecraft.world.phys.Vec3;
|
import net.minecraft.world.phys.Vec3;
|
||||||
|
|
||||||
public class BatchingEngine extends AbstractEngine {
|
public class BatchingEngine extends AbstractEngine {
|
||||||
|
@ -30,20 +32,24 @@ public class BatchingEngine extends AbstractEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beginFrame(TaskExecutor executor, RenderContext context) {
|
public Plan planThisFrame(RenderContext context) {
|
||||||
transformManager.flush();
|
return PlanUtil.of(transformManager::flush)
|
||||||
|
.then(planTransformers(context));
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void submitTasks(TaskExecutor executor, PoseStack.Pose matrices, ClientLevel level) {
|
private Plan planTransformers(RenderContext context) {
|
||||||
for (var transformSetEntry : transformManager.getTransformSetsView().entrySet()) {
|
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<Plan>();
|
||||||
|
|
||||||
|
for (var transformSetEntry : transformManager.getTransformSetsView()
|
||||||
|
.entrySet()) {
|
||||||
var stage = transformSetEntry.getKey();
|
var stage = transformSetEntry.getKey();
|
||||||
var transformSet = transformSetEntry.getValue();
|
var transformSet = transformSetEntry.getValue();
|
||||||
|
|
||||||
|
@ -66,15 +72,21 @@ public class BatchingEngine extends AbstractEngine {
|
||||||
|
|
||||||
int startVertex = 0;
|
int startVertex = 0;
|
||||||
for (var transformCall : transformCalls) {
|
for (var transformCall : transformCalls) {
|
||||||
transformCall.submitTasks(executor, buffer, startVertex, matrices, level);
|
plans.add(transformCall.getPlan(buffer, startVertex, matrices, level));
|
||||||
startVertex += transformCall.getTotalVertexCount();
|
startVertex += transformCall.getTotalVertexCount();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new NestedPlan(plans);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void renderStage(TaskExecutor executor, RenderContext context, RenderStage stage) {
|
public void renderStage(TaskExecutor executor, RenderContext context, RenderStage stage) {
|
||||||
|
if (!drawTracker.hasStage(stage)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
executor.syncPoint();
|
||||||
drawTracker.draw(stage);
|
drawTracker.draw(stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
package com.jozufozu.flywheel.backend.engine.batching;
|
package com.jozufozu.flywheel.backend.engine.batching;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import com.jozufozu.flywheel.api.instance.Instance;
|
import com.jozufozu.flywheel.api.instance.Instance;
|
||||||
import com.jozufozu.flywheel.api.instance.InstanceVertexTransformer;
|
import com.jozufozu.flywheel.api.instance.InstanceVertexTransformer;
|
||||||
import com.jozufozu.flywheel.api.material.Material;
|
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.MutableVertexList;
|
||||||
import com.jozufozu.flywheel.api.vertex.ReusableVertexList;
|
import com.jozufozu.flywheel.api.vertex.ReusableVertexList;
|
||||||
|
import com.jozufozu.flywheel.lib.task.SimplePlan;
|
||||||
import com.jozufozu.flywheel.lib.vertex.VertexTransformations;
|
import com.jozufozu.flywheel.lib.vertex.VertexTransformations;
|
||||||
import com.mojang.blaze3d.vertex.PoseStack;
|
import com.mojang.blaze3d.vertex.PoseStack;
|
||||||
import com.mojang.math.Matrix3f;
|
import com.mojang.math.Matrix3f;
|
||||||
|
@ -40,9 +42,11 @@ public class TransformCall<I extends Instance> {
|
||||||
instancer.update();
|
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();
|
int instances = instancer.getInstanceCount();
|
||||||
|
|
||||||
|
var out = new ArrayList<Runnable>();
|
||||||
|
|
||||||
while (instances > 0) {
|
while (instances > 0) {
|
||||||
int end = instances;
|
int end = instances;
|
||||||
instances -= 512;
|
instances -= 512;
|
||||||
|
@ -52,8 +56,9 @@ public class TransformCall<I extends Instance> {
|
||||||
ReusableVertexList sub = buffer.slice(startVertex, vertexCount);
|
ReusableVertexList sub = buffer.slice(startVertex, vertexCount);
|
||||||
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) {
|
public void transformRange(ReusableVertexList vertexList, int from, int to, PoseStack.Pose matrices, ClientLevel level) {
|
||||||
|
|
|
@ -185,4 +185,7 @@ public class IndirectCullingGroup<I extends Instance> {
|
||||||
meshPool.delete();
|
meshPool.delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasStage(RenderStage stage) {
|
||||||
|
return drawSet.contains(stage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,6 +73,15 @@ public class IndirectDrawManager {
|
||||||
initializedInstancers.add(instancer);
|
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) {
|
private record UninitializedInstancer(IndirectInstancer<?> instancer, Model model, RenderStage stage) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,12 @@ import com.jozufozu.flywheel.api.instance.Instance;
|
||||||
import com.jozufozu.flywheel.api.instance.InstanceType;
|
import com.jozufozu.flywheel.api.instance.InstanceType;
|
||||||
import com.jozufozu.flywheel.api.instance.Instancer;
|
import com.jozufozu.flywheel.api.instance.Instancer;
|
||||||
import com.jozufozu.flywheel.api.model.Model;
|
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.api.task.TaskExecutor;
|
||||||
import com.jozufozu.flywheel.backend.engine.AbstractEngine;
|
import com.jozufozu.flywheel.backend.engine.AbstractEngine;
|
||||||
import com.jozufozu.flywheel.gl.GlStateTracker;
|
import com.jozufozu.flywheel.gl.GlStateTracker;
|
||||||
import com.jozufozu.flywheel.gl.GlTextureUnit;
|
import com.jozufozu.flywheel.gl.GlTextureUnit;
|
||||||
|
import com.jozufozu.flywheel.lib.task.PlanUtil;
|
||||||
import com.mojang.blaze3d.systems.RenderSystem;
|
import com.mojang.blaze3d.systems.RenderSystem;
|
||||||
|
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
|
@ -31,14 +33,24 @@ public class IndirectEngine extends AbstractEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beginFrame(TaskExecutor executor, RenderContext context) {
|
public Plan planThisFrame(RenderContext context) {
|
||||||
try (var restoreState = GlStateTracker.getRestoreState()) {
|
return PlanUtil.onMainThread(this::flushDrawManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flushDrawManager() {
|
||||||
|
try (var state = GlStateTracker.getRestoreState()) {
|
||||||
drawManager.flush();
|
drawManager.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void renderStage(TaskExecutor executor, RenderContext context, RenderStage stage) {
|
public void renderStage(TaskExecutor executor, RenderContext context, RenderStage stage) {
|
||||||
|
if (!drawManager.hasStage(stage)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
executor.syncPoint();
|
||||||
|
|
||||||
try (var restoreState = GlStateTracker.getRestoreState()) {
|
try (var restoreState = GlStateTracker.getRestoreState()) {
|
||||||
setup();
|
setup();
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import com.jozufozu.flywheel.api.instance.Instance;
|
||||||
import com.jozufozu.flywheel.api.instance.InstanceType;
|
import com.jozufozu.flywheel.api.instance.InstanceType;
|
||||||
import com.jozufozu.flywheel.api.instance.Instancer;
|
import com.jozufozu.flywheel.api.instance.Instancer;
|
||||||
import com.jozufozu.flywheel.api.model.Model;
|
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.api.task.TaskExecutor;
|
||||||
import com.jozufozu.flywheel.backend.Pipelines;
|
import com.jozufozu.flywheel.backend.Pipelines;
|
||||||
import com.jozufozu.flywheel.backend.compile.FlwCompiler;
|
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.GlStateTracker;
|
||||||
import com.jozufozu.flywheel.gl.GlTextureUnit;
|
import com.jozufozu.flywheel.gl.GlTextureUnit;
|
||||||
import com.jozufozu.flywheel.lib.material.MaterialIndices;
|
import com.jozufozu.flywheel.lib.material.MaterialIndices;
|
||||||
|
import com.jozufozu.flywheel.lib.task.PlanUtil;
|
||||||
import com.mojang.blaze3d.systems.RenderSystem;
|
import com.mojang.blaze3d.systems.RenderSystem;
|
||||||
|
|
||||||
import net.minecraft.client.Minecraft;
|
import net.minecraft.client.Minecraft;
|
||||||
|
@ -38,8 +40,12 @@ public class InstancingEngine extends AbstractEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void beginFrame(TaskExecutor executor, RenderContext context) {
|
public Plan planThisFrame(RenderContext context) {
|
||||||
try (var state = GlStateTracker.getRestoreState()) {
|
return PlanUtil.onMainThread(this::flushDrawManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void flushDrawManager() {
|
||||||
|
try (var restoreState = GlStateTracker.getRestoreState()) {
|
||||||
drawManager.flush();
|
drawManager.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,6 +58,8 @@ public class InstancingEngine extends AbstractEngine {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
executor.syncPoint();
|
||||||
|
|
||||||
try (var state = GlStateTracker.getRestoreState()) {
|
try (var state = GlStateTracker.getRestoreState()) {
|
||||||
setup();
|
setup();
|
||||||
|
|
||||||
|
|
|
@ -3,14 +3,18 @@ package com.jozufozu.flywheel.backend.task;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Queue;
|
||||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.jetbrains.annotations.Nullable;
|
import org.jetbrains.annotations.Nullable;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
import com.jozufozu.flywheel.Flywheel;
|
import com.jozufozu.flywheel.Flywheel;
|
||||||
import com.jozufozu.flywheel.api.task.TaskExecutor;
|
import com.jozufozu.flywheel.api.task.TaskExecutor;
|
||||||
|
import com.jozufozu.flywheel.lib.task.WaitGroup;
|
||||||
import com.mojang.logging.LogUtils;
|
import com.mojang.logging.LogUtils;
|
||||||
|
|
||||||
import net.minecraft.util.Mth;
|
import net.minecraft.util.Mth;
|
||||||
|
@ -27,16 +31,13 @@ public class ParallelTaskExecutor implements TaskExecutor {
|
||||||
* If set to false, the executor will shut down.
|
* If set to false, the executor will shut down.
|
||||||
*/
|
*/
|
||||||
private final AtomicBoolean running = new AtomicBoolean(false);
|
private final AtomicBoolean running = new AtomicBoolean(false);
|
||||||
/**
|
|
||||||
* Synchronized via {@link #tasksCompletedNotifier}.
|
|
||||||
*/
|
|
||||||
private int incompleteTaskCounter = 0;
|
|
||||||
|
|
||||||
private final List<WorkerThread> threads = new ArrayList<>();
|
private final List<WorkerThread> threads = new ArrayList<>();
|
||||||
private final Deque<Runnable> taskQueue = new ConcurrentLinkedDeque<>();
|
private final Deque<Runnable> taskQueue = new ConcurrentLinkedDeque<>();
|
||||||
|
private final Queue<Runnable> mainThreadQueue = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
private final Object taskNotifier = new Object();
|
private final Object taskNotifier = new Object();
|
||||||
private final Object tasksCompletedNotifier = new Object();
|
private final WaitGroup waitGroup = new WaitGroup();
|
||||||
|
|
||||||
public ParallelTaskExecutor(String name) {
|
public ParallelTaskExecutor(String name) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
@ -99,27 +100,31 @@ public class ParallelTaskExecutor implements TaskExecutor {
|
||||||
|
|
||||||
threads.clear();
|
threads.clear();
|
||||||
taskQueue.clear();
|
taskQueue.clear();
|
||||||
synchronized (tasksCompletedNotifier) {
|
mainThreadQueue.clear();
|
||||||
incompleteTaskCounter = 0;
|
waitGroup._reset();
|
||||||
tasksCompletedNotifier.notifyAll();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void execute(Runnable task) {
|
public void execute(@NotNull Runnable task) {
|
||||||
if (!running.get()) {
|
if (!running.get()) {
|
||||||
throw new IllegalStateException("Executor is stopped");
|
throw new IllegalStateException("Executor is stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
waitGroup.add();
|
||||||
taskQueue.add(task);
|
taskQueue.add(task);
|
||||||
|
|
||||||
synchronized (tasksCompletedNotifier) {
|
synchronized (taskNotifier) {
|
||||||
incompleteTaskCounter++;
|
taskNotifier.notifyAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void scheduleForMainThread(Runnable runnable) {
|
||||||
|
if (!running.get()) {
|
||||||
|
throw new IllegalStateException("Executor is stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
synchronized (taskNotifier) {
|
mainThreadQueue.add(runnable);
|
||||||
taskNotifier.notify();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -128,19 +133,21 @@ public class ParallelTaskExecutor implements TaskExecutor {
|
||||||
@Override
|
@Override
|
||||||
public void syncPoint() {
|
public void syncPoint() {
|
||||||
Runnable task;
|
Runnable task;
|
||||||
|
while (true) {
|
||||||
// Finish everyone else's work...
|
if ((task = mainThreadQueue.poll()) != null) {
|
||||||
while ((task = taskQueue.pollLast()) != null) {
|
// Prioritize main thread tasks.
|
||||||
processTask(task);
|
processMainThreadTask(task);
|
||||||
}
|
} else if ((task = taskQueue.pollLast()) != null) {
|
||||||
|
// then work on tasks from the queue.
|
||||||
// and wait for any stragglers.
|
processTask(task);
|
||||||
synchronized (tasksCompletedNotifier) {
|
} else {
|
||||||
while (incompleteTaskCounter > 0) {
|
// then wait for the other threads to finish.
|
||||||
try {
|
waitGroup.await();
|
||||||
tasksCompletedNotifier.wait();
|
// at this point there will be no more tasks in the queue, but
|
||||||
} catch (InterruptedException e) {
|
// 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() {
|
public void discardAndAwait() {
|
||||||
// Discard everyone else's work...
|
// Discard everyone else's work...
|
||||||
while (taskQueue.pollLast() != null) {
|
while (taskQueue.pollLast() != null) {
|
||||||
synchronized (tasksCompletedNotifier) {
|
waitGroup.done();
|
||||||
if (--incompleteTaskCounter == 0) {
|
|
||||||
tasksCompletedNotifier.notifyAll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// and wait for any stragglers.
|
// ...wait for any stragglers...
|
||||||
synchronized (tasksCompletedNotifier) {
|
waitGroup.await();
|
||||||
while (incompleteTaskCounter > 0) {
|
// ...and clear the main thread queue.
|
||||||
try {
|
mainThreadQueue.clear();
|
||||||
tasksCompletedNotifier.wait();
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -185,18 +182,21 @@ public class ParallelTaskExecutor implements TaskExecutor {
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: task context
|
|
||||||
private void processTask(Runnable task) {
|
private void processTask(Runnable task) {
|
||||||
try {
|
try {
|
||||||
task.run();
|
task.run();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Flywheel.LOGGER.error("Error running task", e);
|
Flywheel.LOGGER.error("Error running task", e);
|
||||||
} finally {
|
} finally {
|
||||||
synchronized (tasksCompletedNotifier) {
|
waitGroup.done();
|
||||||
if (--incompleteTaskCounter == 0) {
|
}
|
||||||
tasksCompletedNotifier.notifyAll();
|
}
|
||||||
}
|
|
||||||
}
|
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 class WorkerThread extends Thread {
|
||||||
private final AtomicBoolean running = ParallelTaskExecutor.this.running;
|
|
||||||
|
|
||||||
public WorkerThread(String name) {
|
public WorkerThread(String name) {
|
||||||
super(name);
|
super(name);
|
||||||
|
@ -221,7 +220,7 @@ public class ParallelTaskExecutor implements TaskExecutor {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
// Run until the executor shuts down
|
// Run until the executor shuts down
|
||||||
while (running.get()) {
|
while (ParallelTaskExecutor.this.running.get()) {
|
||||||
Runnable task = getNextTask();
|
Runnable task = getNextTask();
|
||||||
|
|
||||||
if (task == null) {
|
if (task == null) {
|
||||||
|
|
|
@ -13,6 +13,11 @@ public class SerialTaskExecutor implements TaskExecutor {
|
||||||
task.run();
|
task.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void scheduleForMainThread(Runnable runnable) {
|
||||||
|
runnable.run();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void syncPoint() {
|
public void syncPoint() {
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ public class EntityWorldHandler {
|
||||||
|
|
||||||
if (FlwUtil.canUseVisualization(level)) {
|
if (FlwUtil.canUseVisualization(level)) {
|
||||||
VisualizedRenderDispatcher.getEntities(level)
|
VisualizedRenderDispatcher.getEntities(level)
|
||||||
.remove(event.getEntity());
|
.queueRemove(event.getEntity());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import com.jozufozu.flywheel.api.backend.BackendManager;
|
||||||
import com.jozufozu.flywheel.api.backend.Engine;
|
import com.jozufozu.flywheel.api.backend.Engine;
|
||||||
import com.jozufozu.flywheel.api.event.RenderContext;
|
import com.jozufozu.flywheel.api.event.RenderContext;
|
||||||
import com.jozufozu.flywheel.api.event.RenderStage;
|
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.DynamicVisual;
|
||||||
import com.jozufozu.flywheel.api.visual.Effect;
|
import com.jozufozu.flywheel.api.visual.Effect;
|
||||||
import com.jozufozu.flywheel.api.visual.TickableVisual;
|
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.EntityVisualManager;
|
||||||
import com.jozufozu.flywheel.impl.visualization.manager.VisualManager;
|
import com.jozufozu.flywheel.impl.visualization.manager.VisualManager;
|
||||||
import com.jozufozu.flywheel.lib.math.MatrixUtil;
|
import com.jozufozu.flywheel.lib.math.MatrixUtil;
|
||||||
|
import com.jozufozu.flywheel.lib.task.PlanUtil;
|
||||||
|
|
||||||
import net.minecraft.core.Vec3i;
|
import net.minecraft.core.Vec3i;
|
||||||
import net.minecraft.world.entity.Entity;
|
import net.minecraft.world.entity.Entity;
|
||||||
|
@ -70,9 +72,13 @@ public class VisualWorld implements AutoCloseable {
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
public void tick(double cameraX, double cameraY, double cameraZ) {
|
public void tick(double cameraX, double cameraY, double cameraZ) {
|
||||||
blockEntities.tick(taskExecutor, cameraX, cameraY, cameraZ);
|
taskExecutor.syncPoint();
|
||||||
entities.tick(taskExecutor, cameraX, cameraY, cameraZ);
|
|
||||||
effects.tick(taskExecutor, cameraX, cameraY, cameraZ);
|
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 {
|
||||||
* </p>
|
* </p>
|
||||||
*/
|
*/
|
||||||
public void beginFrame(RenderContext context) {
|
public void beginFrame(RenderContext context) {
|
||||||
boolean originChanged = engine.updateRenderOrigin(context.camera());
|
|
||||||
|
|
||||||
if (originChanged) {
|
|
||||||
blockEntities.recreateAll();
|
|
||||||
entities.recreateAll();
|
|
||||||
effects.recreateAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
taskExecutor.syncPoint();
|
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();
|
Vec3i renderOrigin = engine.renderOrigin();
|
||||||
var cameraPos = context.camera()
|
var cameraPos = context.camera()
|
||||||
.getPosition();
|
.getPosition();
|
||||||
|
@ -106,19 +112,16 @@ public class VisualWorld implements AutoCloseable {
|
||||||
proj.translate((float) (renderOrigin.getX() - cameraX), (float) (renderOrigin.getY() - cameraY), (float) (renderOrigin.getZ() - cameraZ));
|
proj.translate((float) (renderOrigin.getX() - cameraX), (float) (renderOrigin.getY() - cameraY), (float) (renderOrigin.getZ() - cameraZ));
|
||||||
FrustumIntersection frustum = new FrustumIntersection(proj);
|
FrustumIntersection frustum = new FrustumIntersection(proj);
|
||||||
|
|
||||||
blockEntities.beginFrame(taskExecutor, cameraX, cameraY, cameraZ, frustum);
|
return blockEntities.planThisFrame(cameraX, cameraY, cameraZ, frustum)
|
||||||
entities.beginFrame(taskExecutor, cameraX, cameraY, cameraZ, frustum);
|
.and(entities.planThisFrame(cameraX, cameraY, cameraZ, frustum))
|
||||||
effects.beginFrame(taskExecutor, cameraX, cameraY, cameraZ, frustum);
|
.and(effects.planThisFrame(cameraX, cameraY, cameraZ, frustum));
|
||||||
}
|
}
|
||||||
|
|
||||||
engine.beginFrame(taskExecutor, context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Draw all visuals for the given stage.
|
* Draw all visuals for the given stage.
|
||||||
*/
|
*/
|
||||||
public void renderStage(RenderContext context, RenderStage stage) {
|
public void renderStage(RenderContext context, RenderStage stage) {
|
||||||
taskExecutor.syncPoint();
|
|
||||||
engine.renderStage(taskExecutor, context, stage);
|
engine.renderStage(taskExecutor, context, stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -161,7 +161,7 @@ public class VisualizedRenderDispatcher {
|
||||||
// Block entities are loaded while chunks are baked.
|
// 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.
|
// Entities are loaded with the level, so when chunks are reloaded they need to be re-added.
|
||||||
ClientLevelExtension.getAllLoadedEntities(level)
|
ClientLevelExtension.getAllLoadedEntities(level)
|
||||||
.forEach(world.getEntities()::add);
|
.forEach(world.getEntities()::queueAdd);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T extends BlockEntity> boolean tryAddBlockEntity(T blockEntity) {
|
public static <T extends BlockEntity> boolean tryAddBlockEntity(T blockEntity) {
|
||||||
|
|
|
@ -1,22 +1,21 @@
|
||||||
package com.jozufozu.flywheel.impl.visualization.manager;
|
package com.jozufozu.flywheel.impl.visualization.manager;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Queue;
|
import java.util.Queue;
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
import java.util.function.Consumer;
|
|
||||||
|
|
||||||
import org.joml.FrustumIntersection;
|
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.DynamicVisual;
|
||||||
import com.jozufozu.flywheel.api.visual.TickableVisual;
|
import com.jozufozu.flywheel.api.visual.TickableVisual;
|
||||||
import com.jozufozu.flywheel.api.visual.Visual;
|
|
||||||
import com.jozufozu.flywheel.config.FlwConfig;
|
import com.jozufozu.flywheel.config.FlwConfig;
|
||||||
import com.jozufozu.flywheel.impl.visualization.ratelimit.BandedPrimeLimiter;
|
import com.jozufozu.flywheel.impl.visualization.ratelimit.BandedPrimeLimiter;
|
||||||
import com.jozufozu.flywheel.impl.visualization.ratelimit.DistanceUpdateLimiter;
|
import com.jozufozu.flywheel.impl.visualization.ratelimit.DistanceUpdateLimiter;
|
||||||
import com.jozufozu.flywheel.impl.visualization.ratelimit.NonLimiter;
|
import com.jozufozu.flywheel.impl.visualization.ratelimit.NonLimiter;
|
||||||
import com.jozufozu.flywheel.impl.visualization.storage.Storage;
|
import com.jozufozu.flywheel.impl.visualization.storage.Storage;
|
||||||
import com.jozufozu.flywheel.impl.visualization.storage.Transaction;
|
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<T> {
|
public abstract class VisualManager<T> {
|
||||||
private final Queue<Transaction<T>> queue = new ConcurrentLinkedQueue<>();
|
private final Queue<Transaction<T>> queue = new ConcurrentLinkedQueue<>();
|
||||||
|
@ -48,14 +47,6 @@ public abstract class VisualManager<T> {
|
||||||
return getStorage().getAllVisuals().size();
|
return getStorage().getAllVisuals().size();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void add(T obj) {
|
|
||||||
if (!getStorage().willAccept(obj)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
getStorage().add(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void queueAdd(T obj) {
|
public void queueAdd(T obj) {
|
||||||
if (!getStorage().willAccept(obj)) {
|
if (!getStorage().willAccept(obj)) {
|
||||||
return;
|
return;
|
||||||
|
@ -64,33 +55,10 @@ public abstract class VisualManager<T> {
|
||||||
queue.add(Transaction.add(obj));
|
queue.add(Transaction.add(obj));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void remove(T obj) {
|
|
||||||
getStorage().remove(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void queueRemove(T obj) {
|
public void queueRemove(T obj) {
|
||||||
queue.add(Transaction.remove(obj));
|
queue.add(Transaction.remove(obj));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the visual associated with an object.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* 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}.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @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) {
|
public void queueUpdate(T obj) {
|
||||||
if (!getStorage().willAccept(obj)) {
|
if (!getStorage().willAccept(obj)) {
|
||||||
return;
|
return;
|
||||||
|
@ -115,58 +83,32 @@ public abstract class VisualManager<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public Plan planThisTick(double cameraX, double cameraY, double cameraZ) {
|
||||||
* Ticks the VisualManager.
|
return SimplePlan.of(() -> {
|
||||||
*
|
tickLimiter.tick();
|
||||||
* <p>
|
processQueue();
|
||||||
* {@link TickableVisual}s get ticked.
|
})
|
||||||
* <br>
|
.then(RunOnAllPlan.of(getStorage()::getTickableVisuals, instance -> tickInstance(instance, cameraX, cameraY, cameraZ)));
|
||||||
* Queued updates are processed.
|
|
||||||
* </p>
|
|
||||||
*/
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void tickVisual(TickableVisual visual, double cameraX, double cameraY, double cameraZ) {
|
protected void tickInstance(TickableVisual instance, double cameraX, double cameraY, double cameraZ) {
|
||||||
if (!visual.decreaseTickRateWithDistance() || tickLimiter.shouldUpdate(visual.distanceSquared(cameraX, cameraY, cameraZ))) {
|
if (!instance.decreaseTickRateWithDistance() || tickLimiter.shouldUpdate(instance.distanceSquared(cameraX, cameraY, cameraZ))) {
|
||||||
visual.tick();
|
instance.tick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void beginFrame(TaskExecutor executor, double cameraX, double cameraY, double cameraZ, FrustumIntersection frustum) {
|
public Plan planThisFrame(double cameraX, double cameraY, double cameraZ, FrustumIntersection frustum) {
|
||||||
frameLimiter.tick();
|
return SimplePlan.of(() -> {
|
||||||
processQueue();
|
frameLimiter.tick();
|
||||||
|
processQueue();
|
||||||
var visuals = getStorage().getDynamicVisuals();
|
})
|
||||||
distributeWork(executor, visuals, visual -> updateVisual(visual, cameraX, cameraY, cameraZ, frustum));
|
.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) {
|
protected void updateInstance(DynamicVisual instance, double cameraX, double cameraY, double cameraZ, FrustumIntersection frustum) {
|
||||||
if (!visual.decreaseFramerateWithDistance() || frameLimiter.shouldUpdate(visual.distanceSquared(cameraX, cameraY, cameraZ))) {
|
if (!instance.decreaseFramerateWithDistance() || frameLimiter.shouldUpdate(instance.distanceSquared(cameraX, cameraY, cameraZ))) {
|
||||||
if (visual.isVisible(frustum)) {
|
if (instance.isVisible(frustum)) {
|
||||||
visual.beginFrame();
|
instance.beginFrame();
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static <V> void distributeWork(TaskExecutor executor, List<V> visuals, Consumer<V> 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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,9 +5,7 @@ import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import com.jozufozu.flywheel.backend.task.FlwTaskExecutor;
|
|
||||||
import com.jozufozu.flywheel.lib.box.ImmutableBox;
|
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.FlwUtil;
|
||||||
import com.jozufozu.flywheel.util.WorldAttached;
|
import com.jozufozu.flywheel.util.WorldAttached;
|
||||||
|
|
||||||
|
@ -30,6 +28,8 @@ public class LightUpdater {
|
||||||
private final WeakContainmentMultiMap<LightListener> listenersBySection = new WeakContainmentMultiMap<>();
|
private final WeakContainmentMultiMap<LightListener> listenersBySection = new WeakContainmentMultiMap<>();
|
||||||
private final Set<TickingLightListener> tickingListeners = FlwUtil.createWeakHashSet();
|
private final Set<TickingLightListener> tickingListeners = FlwUtil.createWeakHashSet();
|
||||||
|
|
||||||
|
private final Queue<LightListener> queue = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
public static LightUpdater get(LevelAccessor level) {
|
public static LightUpdater get(LevelAccessor level) {
|
||||||
if (LightUpdated.receivesLightUpdates(level)) {
|
if (LightUpdated.receivesLightUpdates(level)) {
|
||||||
// The level is valid, add it to the map.
|
// The level is valid, add it to the map.
|
||||||
|
@ -45,8 +45,8 @@ public class LightUpdater {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void tick() {
|
public void tick() {
|
||||||
|
processQueue();
|
||||||
tickSerial();
|
tickSerial();
|
||||||
//tickParallel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void tickSerial() {
|
private void tickSerial() {
|
||||||
|
@ -57,27 +57,26 @@ public class LightUpdater {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void tickParallel() {
|
|
||||||
Queue<LightListener> 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.
|
* Add a listener.
|
||||||
*
|
*
|
||||||
* @param listener The object that wants to receive light update notifications.
|
* @param listener The object that wants to receive light update notifications.
|
||||||
*/
|
*/
|
||||||
public void addListener(LightListener listener) {
|
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));
|
tickingListeners.add(((TickingLightListener) listener));
|
||||||
|
}
|
||||||
|
|
||||||
ImmutableBox box = listener.getVolume();
|
ImmutableBox box = listener.getVolume();
|
||||||
|
|
||||||
|
@ -111,9 +110,13 @@ public class LightUpdater {
|
||||||
* @param pos The section position where light changed.
|
* @param pos The section position where light changed.
|
||||||
*/
|
*/
|
||||||
public void onLightUpdate(LightLayer type, SectionPos pos) {
|
public void onLightUpdate(LightLayer type, SectionPos pos) {
|
||||||
|
processQueue();
|
||||||
|
|
||||||
Set<LightListener> listeners = listenersBySection.get(pos.asLong());
|
Set<LightListener> listeners = listenersBySection.get(pos.asLong());
|
||||||
|
|
||||||
if (listeners == null || listeners.isEmpty()) return;
|
if (listeners == null || listeners.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
listeners.removeIf(LightListener::isInvalid);
|
listeners.removeIf(LightListener::isInvalid);
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
package com.jozufozu.flywheel.lib.model;
|
package com.jozufozu.flywheel.lib.model;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
import com.jozufozu.flywheel.api.event.ReloadRenderersEvent;
|
import com.jozufozu.flywheel.api.event.ReloadRenderersEvent;
|
||||||
import com.jozufozu.flywheel.api.model.Model;
|
import com.jozufozu.flywheel.api.model.Model;
|
||||||
|
@ -16,9 +16,9 @@ import net.minecraft.core.Direction;
|
||||||
import net.minecraft.world.level.block.state.BlockState;
|
import net.minecraft.world.level.block.state.BlockState;
|
||||||
|
|
||||||
public final class Models {
|
public final class Models {
|
||||||
private static final Map<BlockState, Model> BLOCK_STATE = new HashMap<>();
|
private static final Map<BlockState, Model> BLOCK_STATE = new ConcurrentHashMap<>();
|
||||||
private static final Map<PartialModel, Model> PARTIAL = new HashMap<>();
|
private static final Map<PartialModel, Model> PARTIAL = new ConcurrentHashMap<>();
|
||||||
private static final Map<Pair<PartialModel, Direction>, Model> PARTIAL_DIR = new HashMap<>();
|
private static final Map<Pair<PartialModel, Direction>, Model> PARTIAL_DIR = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public static Model block(BlockState state) {
|
public static Model block(BlockState state) {
|
||||||
return BLOCK_STATE.computeIfAbsent(state, it -> new BlockModelBuilder(it).build());
|
return BLOCK_STATE.computeIfAbsent(state, it -> new BlockModelBuilder(it).build());
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
103
src/main/java/com/jozufozu/flywheel/lib/task/NestedPlan.java
Normal file
103
src/main/java/com/jozufozu/flywheel/lib/task/NestedPlan.java
Normal file
|
@ -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<Plan> 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.<Plan>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<Runnable>();
|
||||||
|
var simplifiedPlans = new ArrayList<Plan>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
24
src/main/java/com/jozufozu/flywheel/lib/task/PlanUtil.java
Normal file
24
src/main/java/com/jozufozu/flywheel/lib/task/PlanUtil.java
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<T>(Supplier<List<T>> listSupplier, Consumer<T> action) implements Plan {
|
||||||
|
public static <T> Plan of(Supplier<List<T>> iterable, Consumer<T> 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<T> 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<T> suppliedList, Runnable onCompletion) {
|
||||||
|
for (T t : suppliedList) {
|
||||||
|
action.accept(t);
|
||||||
|
}
|
||||||
|
onCompletion.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getChunkingThreshold() {
|
||||||
|
return 256;
|
||||||
|
}
|
||||||
|
}
|
37
src/main/java/com/jozufozu/flywheel/lib/task/SimplePlan.java
Normal file
37
src/main/java/com/jozufozu/flywheel/lib/task/SimplePlan.java
Normal file
|
@ -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<Runnable> 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
src/main/java/com/jozufozu/flywheel/lib/task/UnitPlan.java
Normal file
16
src/main/java/com/jozufozu/flywheel/lib/task/UnitPlan.java
Normal file
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
52
src/main/java/com/jozufozu/flywheel/lib/task/WaitGroup.java
Normal file
52
src/main/java/com/jozufozu/flywheel/lib/task/WaitGroup.java
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<Runnable> tasks, Executor executor) {
|
|
||||||
tasks.forEachRemaining(executor::execute);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void run(Iterator<Runnable> 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<Runnable> tasks;
|
|
||||||
|
|
||||||
public Builder() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public <T> Builder addTasks(Stream<T> iterable, Consumer<T> consumer) {
|
|
||||||
return addTasks(iterable.map(it -> () -> consumer.accept(it)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Builder addTasks(Stream<Runnable> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -28,6 +28,6 @@ public class VisualAddMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
VisualizedRenderDispatcher.getBlockEntities(level)
|
VisualizedRenderDispatcher.getBlockEntities(level)
|
||||||
.add(blockEntity);
|
.queueAdd(blockEntity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,6 @@ public class VisualRemoveMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
VisualizedRenderDispatcher.getBlockEntities(level)
|
VisualizedRenderDispatcher.getBlockEntities(level)
|
||||||
.remove((BlockEntity) (Object) this);
|
.queueRemove((BlockEntity) (Object) this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,6 @@ public class VisualUpdateMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
VisualizedRenderDispatcher.getBlockEntities(level)
|
VisualizedRenderDispatcher.getBlockEntities(level)
|
||||||
.update(blockEntity);
|
.queueUpdate(blockEntity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue