From 9f029041a4d0a6a4b47683903d962ed7f827b563 Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Sun, 26 Nov 2023 13:49:54 -0800 Subject: [PATCH] Consistent batches - Reintroduce BatchedDrawManager. - BatchingEngine no longer implements plan itself, instead uses composition of other plans. - Add DynamicNestedPlan, runs many plans provided at execution time. - Add ContextFunction and ContextSupplier to match *Consumer and *Runnable. - Add unit tests for DynamicNestedPlan and IfElsePlan. --- .../engine/batching/BatchedDrawManager.java | 81 ++++++++++++++++ .../engine/batching/BatchingEngine.java | 97 +++---------------- .../flywheel/lib/task/ContextFunction.java | 13 +++ .../flywheel/lib/task/ContextSupplier.java | 17 ++++ .../flywheel/lib/task/DynamicNestedPlan.java | 38 ++++++++ .../flywheel/lib/task/PlanExecutionTest.java | 77 ++++++++++++--- 6 files changed, 227 insertions(+), 96 deletions(-) create mode 100644 src/main/java/com/jozufozu/flywheel/backend/engine/batching/BatchedDrawManager.java create mode 100644 src/main/java/com/jozufozu/flywheel/lib/task/ContextFunction.java create mode 100644 src/main/java/com/jozufozu/flywheel/lib/task/ContextSupplier.java create mode 100644 src/main/java/com/jozufozu/flywheel/lib/task/DynamicNestedPlan.java diff --git a/src/main/java/com/jozufozu/flywheel/backend/engine/batching/BatchedDrawManager.java b/src/main/java/com/jozufozu/flywheel/backend/engine/batching/BatchedDrawManager.java new file mode 100644 index 000000000..d788aba6f --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/backend/engine/batching/BatchedDrawManager.java @@ -0,0 +1,81 @@ +package com.jozufozu.flywheel.backend.engine.batching; + +import java.util.Collection; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.Map; + +import com.jozufozu.flywheel.api.event.RenderStage; +import com.jozufozu.flywheel.api.instance.Instance; +import com.jozufozu.flywheel.api.instance.InstanceType; +import com.jozufozu.flywheel.api.model.Mesh; +import com.jozufozu.flywheel.api.model.Model; +import com.jozufozu.flywheel.api.task.TaskExecutor; +import com.jozufozu.flywheel.backend.engine.InstancerKey; +import com.jozufozu.flywheel.backend.engine.InstancerStorage; +import com.mojang.blaze3d.vertex.VertexFormat; + +import net.minecraft.client.renderer.RenderType; + +class BatchedDrawManager extends InstancerStorage> { + private final BatchedDrawTracker drawTracker = new BatchedDrawTracker(); + private final Map stagePlans = new EnumMap<>(RenderStage.class); + private final Map meshPools = new HashMap<>(); + + public Collection getStagePlans() { + return stagePlans.values(); + } + + public void renderStage(TaskExecutor executor, RenderStage stage) { + var stagePlan = stagePlans.get(stage); + + if (stagePlan == null) { + return; + } + + executor.syncUntil(stagePlan.flag::isRaised); + stagePlan.flag.lower(); + + drawTracker.draw(stage); + } + + @Override + protected BatchedInstancer create(InstanceType type) { + return new BatchedInstancer<>(type); + } + + @Override + protected void add(InstancerKey key, BatchedInstancer instancer, Model model, RenderStage stage) { + var stagePlan = stagePlans.computeIfAbsent(stage, renderStage -> new BatchedStagePlan(renderStage, drawTracker)); + var meshes = model.getMeshes(); + for (var entry : meshes.entrySet()) { + var material = entry.getKey(); + RenderType renderType = material.getFallbackRenderType(); + var transformCall = new TransformCall<>(instancer, material, alloc(entry.getValue(), renderType.format())); + stagePlan.put(renderType, transformCall); + } + } + + @Override + public void flush() { + super.flush(); + + for (var pool : meshPools.values()) { + pool.flush(); + } + } + + @Override + public void invalidate() { + super.invalidate(); + + meshPools.values() + .forEach(BatchedMeshPool::delete); + meshPools.clear(); + } + + private BatchedMeshPool.BufferedMesh alloc(Mesh mesh, VertexFormat format) { + return meshPools.computeIfAbsent(format, BatchedMeshPool::new) + .alloc(mesh); + } +} 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 0122ceeac..2d83f7140 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,55 +1,23 @@ package com.jozufozu.flywheel.backend.engine.batching; -import java.util.EnumMap; -import java.util.HashMap; import java.util.List; -import java.util.Map; import com.jozufozu.flywheel.api.event.RenderContext; import com.jozufozu.flywheel.api.event.RenderStage; import com.jozufozu.flywheel.api.instance.Instance; -import com.jozufozu.flywheel.api.instance.InstanceType; -import com.jozufozu.flywheel.api.model.Mesh; -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.backend.engine.AbstractInstancer; -import com.jozufozu.flywheel.backend.engine.InstancerKey; import com.jozufozu.flywheel.backend.engine.InstancerStorage; +import com.jozufozu.flywheel.lib.task.DynamicNestedPlan; import com.jozufozu.flywheel.lib.task.Flag; +import com.jozufozu.flywheel.lib.task.MapContextPlan; import com.jozufozu.flywheel.lib.task.NamedFlag; -import com.jozufozu.flywheel.lib.task.SimplyComposedPlan; -import com.jozufozu.flywheel.lib.task.Synchronizer; -import com.mojang.blaze3d.vertex.VertexFormat; +import com.jozufozu.flywheel.lib.task.SimplePlan; -import net.minecraft.client.renderer.RenderType; - -public class BatchingEngine extends AbstractEngine implements SimplyComposedPlan { - private final BatchedDrawTracker drawTracker = new BatchedDrawTracker(); - - // TODO: reintroduce BatchedDrawManager - private final InstancerStorage> storage = new InstancerStorage<>() { - @Override - protected BatchedInstancer create(InstanceType type) { - return new BatchedInstancer<>(type); - } - - @Override - protected void add(InstancerKey key, BatchedInstancer instancer, Model model, RenderStage stage) { - var stagePlan = stagePlans.computeIfAbsent(stage, renderStage -> new BatchedStagePlan(renderStage, drawTracker)); - var meshes = model.getMeshes(); - for (var entry : meshes.entrySet()) { - var material = entry.getKey(); - RenderType renderType = material.getFallbackRenderType(); - var transformCall = new TransformCall<>(instancer, material, alloc(entry.getValue(), renderType.format())); - stagePlan.put(renderType, transformCall); - } - } - }; - - private final Map stagePlans = new EnumMap<>(RenderStage.class); - private final Map meshPools = new HashMap<>(); +public class BatchingEngine extends AbstractEngine { + private final BatchedDrawManager drawManager = new BatchedDrawManager(); private final Flag flushFlag = new NamedFlag("flushed"); @@ -57,26 +25,13 @@ public class BatchingEngine extends AbstractEngine implements SimplyComposedPlan super(maxOriginDistance); } - @Override - public void execute(TaskExecutor taskExecutor, RenderContext context, Runnable onCompletion) { - flush(); - - // Now it's safe to read stage plans in #renderStage. - flushFlag.raise(); - - BatchContext ctx = BatchContext.create(context, renderOrigin); - - var sync = new Synchronizer(stagePlans.values() - .size(), onCompletion); - - for (var stagePlan : stagePlans.values()) { - stagePlan.execute(taskExecutor, ctx, sync); - } - } - @Override public Plan createFramePlan() { - return this; + return SimplePlan.of(() -> { + drawManager.flush(); + flushFlag.raise(); + }).then(MapContextPlan.map((RenderContext ctx) -> BatchContext.create(ctx, renderOrigin)) + .to(DynamicNestedPlan.of(drawManager::getStagePlans))); } @Override @@ -86,16 +41,7 @@ public class BatchingEngine extends AbstractEngine implements SimplyComposedPlan flushFlag.lower(); } - var stagePlan = stagePlans.get(stage); - - if (stagePlan == null) { - drawTracker.draw(stage); - return; - } - - executor.syncUntil(stagePlan.flag::isRaised); - stagePlan.flag.lower(); - drawTracker.draw(stage); + drawManager.renderStage(executor, stage); } @Override @@ -105,28 +51,11 @@ public class BatchingEngine extends AbstractEngine implements SimplyComposedPlan @Override protected InstancerStorage> getStorage() { - return storage; + return drawManager; } @Override public void delete() { - storage.invalidate(); - - meshPools.values() - .forEach(BatchedMeshPool::delete); - meshPools.clear(); - } - - private void flush() { - storage.flush(); - - for (var pool : meshPools.values()) { - pool.flush(); - } - } - - private BatchedMeshPool.BufferedMesh alloc(Mesh mesh, VertexFormat format) { - return meshPools.computeIfAbsent(format, BatchedMeshPool::new) - .alloc(mesh); + drawManager.invalidate(); } } diff --git a/src/main/java/com/jozufozu/flywheel/lib/task/ContextFunction.java b/src/main/java/com/jozufozu/flywheel/lib/task/ContextFunction.java new file mode 100644 index 000000000..98ce006a8 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/lib/task/ContextFunction.java @@ -0,0 +1,13 @@ +package com.jozufozu.flywheel.lib.task; + +import com.jozufozu.flywheel.api.task.Plan; + +/** + * A function like interface for use with {@link Plan}s. + * @param The context type. + * @param The return type. + */ +@FunctionalInterface +public interface ContextFunction { + R apply(C context); +} diff --git a/src/main/java/com/jozufozu/flywheel/lib/task/ContextSupplier.java b/src/main/java/com/jozufozu/flywheel/lib/task/ContextSupplier.java new file mode 100644 index 000000000..6ee3fbded --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/lib/task/ContextSupplier.java @@ -0,0 +1,17 @@ +package com.jozufozu.flywheel.lib.task; + +/** + * A {@link ContextFunction} that ignores the context object. + * + * @param The context type. + * @param The return type. + */ +@FunctionalInterface +public interface ContextSupplier extends ContextFunction { + R get(); + + @Override + default R apply(C ignored) { + return get(); + } +} diff --git a/src/main/java/com/jozufozu/flywheel/lib/task/DynamicNestedPlan.java b/src/main/java/com/jozufozu/flywheel/lib/task/DynamicNestedPlan.java new file mode 100644 index 000000000..440276220 --- /dev/null +++ b/src/main/java/com/jozufozu/flywheel/lib/task/DynamicNestedPlan.java @@ -0,0 +1,38 @@ +package com.jozufozu.flywheel.lib.task; + +import java.util.Collection; + +import com.jozufozu.flywheel.api.task.Plan; +import com.jozufozu.flywheel.api.task.TaskExecutor; + +/** + * A plan that executes many other plans provided dynamically. + * + * @param plans A function to get a collection of plans based on the context. + * @param The type of the context object. + */ +public record DynamicNestedPlan(ContextFunction>> plans) implements SimplyComposedPlan { + public static Plan of(ContextSupplier>> supplier) { + return new DynamicNestedPlan<>(supplier); + } + + public static Plan of(ContextFunction>> function) { + return new DynamicNestedPlan<>(function); + } + + @Override + public void execute(TaskExecutor taskExecutor, C context, Runnable onCompletion) { + var plans = this.plans.apply(context); + + if (plans.isEmpty()) { + onCompletion.run(); + return; + } + + var sync = new Synchronizer(plans.size(), onCompletion); + + for (var plan : plans) { + plan.execute(taskExecutor, context, sync); + } + } +} diff --git a/src/test/java/com/jozufozu/flywheel/lib/task/PlanExecutionTest.java b/src/test/java/com/jozufozu/flywheel/lib/task/PlanExecutionTest.java index 1e007e4df..2e1c764b8 100644 --- a/src/test/java/com/jozufozu/flywheel/lib/task/PlanExecutionTest.java +++ b/src/test/java/com/jozufozu/flywheel/lib/task/PlanExecutionTest.java @@ -1,5 +1,7 @@ package com.jozufozu.flywheel.lib.task; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -114,29 +116,74 @@ class PlanExecutionTest { } @Test - void unitPlan() { + void emptyPlansDontCallTheExecutor() { var done = new AtomicBoolean(false); UnitPlan.of() .execute(null, Unit.INSTANCE, () -> done.set(true)); Assertions.assertTrue(done.get()); - } - - @Test - void emptyPlan() { - var done = new AtomicBoolean(false); + done.set(false); SimplePlan.of() .execute(null, Unit.INSTANCE, () -> done.set(true)); - Assertions.assertTrue(done.get()); + Assertions.assertTrue(done.get()); done.set(false); + NestedPlan.of() .execute(null, Unit.INSTANCE, () -> done.set(true)); + Assertions.assertTrue(done.get()); } + @Test + void ifElsePlan() { + var branch = new AtomicInteger(0); + + var plan = IfElsePlan.on(b -> b) + .ifTrue(SimplePlan.of(() -> branch.set(1))) + .ifFalse(SimplePlan.of(() -> branch.set(2))) + .plan(); + + runAndWait(plan, true); + + Assertions.assertEquals(1, branch.get()); + + runAndWait(plan, false); + + Assertions.assertEquals(2, branch.get()); + } + + @Test + void dynamicNestedPlan() { + var counter = new AtomicInteger(0); + + List> plans = new ArrayList<>(); + + // We'll re-use this same plan but append to the list of plans it executes. + var plan = DynamicNestedPlan.of(() -> plans); + + runAndWait(plan); + + Assertions.assertEquals(0, counter.get()); + + plans.add(SimplePlan.of(counter::incrementAndGet)); + + runAndWait(plan); + + Assertions.assertEquals(1, counter.get()); + + counter.set(0); + + plans.add(SimplePlan.of(counter::incrementAndGet)); + plans.add(SimplePlan.of(counter::incrementAndGet)); + + runAndWait(plan); + + Assertions.assertEquals(3, counter.get()); + } + @Test void mainThreadPlan() { var done = new AtomicBoolean(false); @@ -213,19 +260,25 @@ class PlanExecutionTest { } public static void runAndWait(Plan plan) { - new TestBarrier(plan).runAndWait(); + new TestBarrier(plan, Unit.INSTANCE).runAndWait(); } - private static final class TestBarrier { - private final Plan plan; + public static void runAndWait(Plan plan, C ctx) { + new TestBarrier<>(plan, ctx).runAndWait(); + } + + private static final class TestBarrier { + private final Plan plan; + private final C ctx; private boolean done = false; - private TestBarrier(Plan plan) { + private TestBarrier(Plan plan, C ctx) { this.plan = plan; + this.ctx = ctx; } public void runAndWait() { - plan.execute(EXECUTOR, Unit.INSTANCE, this::doneWithPlan); + plan.execute(EXECUTOR, ctx, this::doneWithPlan); synchronized (this) { // early exit in case the plan is already done for e.g. UnitPlan