mirror of
https://github.com/Jozufozu/Flywheel.git
synced 2024-12-25 06:26:56 +01:00
Full of red flags
- Add concept of flags to TaskExecutor. - Can raise and lower flags from any thread. - Add TaskExecutor#syncTo - Behaves much like #syncPoint, but exits early as soon as it detects that the requested flag has been raised. - Document all methods in TaskExecutor. - Do not discard tasks when destroying a VisualizationManagerImpl. - Use flags in VisualizationManagerImpl to track frame plan and tick plan completion. - Use flags in BatchingEngine to track stage buffering completion and flush completion. - Synchronization is now needed in BatchedDrawTracker#draw. - Use flags in IndirectEngine and InstancingEngine to track flush completion. - Add unit tests to validate flag behavior. - Rename OnMainThreadPlan -> SyncedPlan.
This commit is contained in:
parent
e7a6052a84
commit
8126a63216
18 changed files with 419 additions and 67 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -5,6 +5,7 @@ run/
|
|||
build/
|
||||
gradle-app.setting
|
||||
out/
|
||||
logs/
|
||||
|
||||
## IntelliJ IDEA
|
||||
|
||||
|
|
|
@ -13,4 +13,22 @@ public enum RenderStage {
|
|||
AFTER_TRANSLUCENT_TERRAIN,
|
||||
AFTER_PARTICLES,
|
||||
AFTER_WEATHER;
|
||||
|
||||
/**
|
||||
* Is this stage the last one to be rendered in the frame?
|
||||
*
|
||||
* @return {@code true} if no other RenderStages will be dispatched this frame.
|
||||
*/
|
||||
public boolean isLast() {
|
||||
return this == values()[values().length - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this stage the first one to be rendered in the frame?
|
||||
*
|
||||
* @return {@code true} if this is the first RenderStage to be dispatched this frame.
|
||||
*/
|
||||
public boolean isFirst() {
|
||||
return this == values()[0];
|
||||
}
|
||||
}
|
||||
|
|
12
src/main/java/com/jozufozu/flywheel/api/task/Flag.java
Normal file
12
src/main/java/com/jozufozu/flywheel/api/task/Flag.java
Normal file
|
@ -0,0 +1,12 @@
|
|||
package com.jozufozu.flywheel.api.task;
|
||||
|
||||
/**
|
||||
* Marker interface for flags that can be raised and lowered in a {@link TaskExecutor}.
|
||||
* <br>
|
||||
* <strong>Warning:</strong> flags will only be considered equal by reference.
|
||||
* This is to allow multiple instances of the same high level structures to exist at once.
|
||||
* <br>
|
||||
* Keep this in mind when using records as flags.
|
||||
*/
|
||||
public interface Flag {
|
||||
}
|
|
@ -4,11 +4,66 @@ import java.util.concurrent.Executor;
|
|||
|
||||
public interface TaskExecutor extends Executor {
|
||||
/**
|
||||
* Wait for all running tasks to finish.
|
||||
* Wait for <em>all</em> running tasks to finish.
|
||||
* <br>
|
||||
* This is useful as a nuclear option, but most of the time you should
|
||||
* try to use {@link Flag flags} and {@link #syncTo(Flag) syncTo}.
|
||||
*/
|
||||
void syncPoint();
|
||||
|
||||
/**
|
||||
* Wait for running tasks, until the given Flag is {@link #raise raised}.
|
||||
* <br>
|
||||
* The flag will remain raised until {@link #lower lowered} manually.
|
||||
*
|
||||
* @param flag The flag to wait for.
|
||||
* @return {@code true} if the flag was encountered. May return false if
|
||||
* this executor runs out of tasks before the flag was raised.
|
||||
*/
|
||||
boolean syncTo(Flag flag);
|
||||
|
||||
/**
|
||||
* Raise a flag indicating a key point in execution.
|
||||
* <br>
|
||||
* If the flag was already raised, this method does nothing.
|
||||
*
|
||||
* @param flag The flag to raise.
|
||||
*/
|
||||
void raise(Flag flag);
|
||||
|
||||
/**
|
||||
* Lower a flag that may have been previously raised.
|
||||
* <br>
|
||||
* If the flag was never raised, this method does nothing.
|
||||
*
|
||||
* @param flag The flag to lower.
|
||||
*/
|
||||
void lower(Flag flag);
|
||||
|
||||
/**
|
||||
* Check if a flag is raised without waiting for it.
|
||||
*
|
||||
* @param flag The flag to check.
|
||||
* @return {@code true} if the flag is raised.
|
||||
*/
|
||||
boolean isRaised(Flag flag);
|
||||
|
||||
/**
|
||||
* Check for the number of threads this executor uses.
|
||||
* <br>
|
||||
* May be helpful when determining how many chunks to divide a task into.
|
||||
*
|
||||
* @return The number of threads this executor uses.
|
||||
*/
|
||||
int getThreadCount();
|
||||
|
||||
void scheduleForMainThread(Runnable runnable);
|
||||
/**
|
||||
* Schedule a task to be run on the main thread.
|
||||
* <br>
|
||||
* This method may be called from any thread, but the runnable will only
|
||||
* be executed once somebody calls either {@link #syncPoint()} or
|
||||
* {@link #syncTo(Flag)}.
|
||||
* @param runnable The task to run.
|
||||
*/
|
||||
void scheduleForSync(Runnable runnable);
|
||||
}
|
||||
|
|
|
@ -52,7 +52,15 @@ public class BatchedDrawTracker {
|
|||
* @param stage The RenderStage to draw.
|
||||
*/
|
||||
public void draw(RenderStage stage) {
|
||||
Set<DrawBuffer> buffers = activeBuffers.get(stage);
|
||||
// This may appear jank, but flag synchronization in BatchingEngine guarantees that
|
||||
// the mapped-to Set will not be modified here. We don't have the same guarantee for
|
||||
// activeBuffers itself, so we need to synchronize to fetch the Set.
|
||||
|
||||
Set<DrawBuffer> buffers;
|
||||
synchronized (activeBuffers) {
|
||||
buffers = activeBuffers.get(stage);
|
||||
}
|
||||
|
||||
for (DrawBuffer buffer : buffers) {
|
||||
_draw(buffer);
|
||||
}
|
||||
|
|
|
@ -7,8 +7,10 @@ import java.util.Map;
|
|||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import com.jozufozu.flywheel.api.event.RenderStage;
|
||||
import com.jozufozu.flywheel.api.task.Flag;
|
||||
import com.jozufozu.flywheel.api.task.TaskExecutor;
|
||||
import com.jozufozu.flywheel.lib.task.SimplyComposedPlan;
|
||||
import com.jozufozu.flywheel.lib.task.StageFlag;
|
||||
import com.jozufozu.flywheel.lib.task.Synchronizer;
|
||||
|
||||
import net.minecraft.client.renderer.RenderType;
|
||||
|
@ -17,24 +19,34 @@ import net.minecraft.client.renderer.RenderType;
|
|||
* All the rendering that happens within a render stage.
|
||||
*/
|
||||
public class BatchedStagePlan implements SimplyComposedPlan<BatchContext> {
|
||||
/**
|
||||
* This flag will be raised when this stage completes execution.
|
||||
*/
|
||||
public final Flag flag;
|
||||
|
||||
private final RenderStage stage;
|
||||
private final BatchedDrawTracker tracker;
|
||||
private final Map<RenderType, BufferPlan> bufferPlans = new HashMap<>();
|
||||
|
||||
public BatchedStagePlan(RenderStage renderStage, BatchedDrawTracker tracker) {
|
||||
stage = renderStage;
|
||||
public BatchedStagePlan(RenderStage stage, BatchedDrawTracker tracker) {
|
||||
this.flag = new StageFlag(stage);
|
||||
this.stage = stage;
|
||||
this.tracker = tracker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(TaskExecutor taskExecutor, BatchContext context, Runnable onCompletion) {
|
||||
if (isEmpty()) {
|
||||
taskExecutor.raise(flag);
|
||||
onCompletion.run();
|
||||
return;
|
||||
}
|
||||
|
||||
taskExecutor.execute(() -> {
|
||||
var sync = new Synchronizer(bufferPlans.size(), onCompletion);
|
||||
var sync = new Synchronizer(bufferPlans.size(), () -> {
|
||||
taskExecutor.raise(flag);
|
||||
onCompletion.run();
|
||||
});
|
||||
|
||||
for (var plan : bufferPlans.values()) {
|
||||
plan.execute(taskExecutor, context, sync);
|
||||
|
|
|
@ -13,10 +13,12 @@ import com.jozufozu.flywheel.api.instance.InstanceType;
|
|||
import com.jozufozu.flywheel.api.instance.Instancer;
|
||||
import com.jozufozu.flywheel.api.model.Mesh;
|
||||
import com.jozufozu.flywheel.api.model.Model;
|
||||
import com.jozufozu.flywheel.api.task.Flag;
|
||||
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.InstancerKey;
|
||||
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;
|
||||
|
@ -31,6 +33,8 @@ public class BatchingEngine extends AbstractEngine implements SimplyComposedPlan
|
|||
private final Map<RenderStage, BatchedStagePlan> stagePlans = new EnumMap<>(RenderStage.class);
|
||||
private final Map<VertexFormat, BatchedMeshPool> meshPools = new HashMap<>();
|
||||
|
||||
private final Flag flushFlag = new NamedFlag("flushed");
|
||||
|
||||
public BatchingEngine(int maxOriginDistance) {
|
||||
super(maxOriginDistance);
|
||||
}
|
||||
|
@ -52,6 +56,9 @@ public class BatchingEngine extends AbstractEngine implements SimplyComposedPlan
|
|||
public void execute(TaskExecutor taskExecutor, RenderContext context, Runnable onCompletion) {
|
||||
flush();
|
||||
|
||||
// Now it's safe to read stage plans in #renderStage.
|
||||
taskExecutor.raise(flushFlag);
|
||||
|
||||
BatchContext ctx = BatchContext.create(context, renderOrigin);
|
||||
|
||||
var sync = new Synchronizer(stagePlans.values()
|
||||
|
@ -69,7 +76,20 @@ public class BatchingEngine extends AbstractEngine implements SimplyComposedPlan
|
|||
|
||||
@Override
|
||||
public void renderStage(TaskExecutor executor, RenderContext context, RenderStage stage) {
|
||||
executor.syncPoint();
|
||||
executor.syncTo(flushFlag);
|
||||
if (stage.isLast()) {
|
||||
executor.lower(flushFlag);
|
||||
}
|
||||
|
||||
var stagePlan = stagePlans.get(stage);
|
||||
|
||||
if (stagePlan == null) {
|
||||
drawTracker.draw(stage);
|
||||
return;
|
||||
}
|
||||
|
||||
executor.syncTo(stagePlan.flag);
|
||||
executor.lower(stagePlan.flag);
|
||||
drawTracker.draw(stage);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,18 +8,22 @@ 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.Flag;
|
||||
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.OnMainThreadPlan;
|
||||
import com.jozufozu.flywheel.lib.task.NamedFlag;
|
||||
import com.jozufozu.flywheel.lib.task.RaisePlan;
|
||||
import com.jozufozu.flywheel.lib.task.SyncedPlan;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
|
||||
public class IndirectEngine extends AbstractEngine {
|
||||
private final IndirectDrawManager drawManager = new IndirectDrawManager();
|
||||
private final Flag flushFlag = new NamedFlag("flushed");
|
||||
|
||||
public IndirectEngine(int maxOriginDistance) {
|
||||
super(maxOriginDistance);
|
||||
|
@ -32,7 +36,8 @@ public class IndirectEngine extends AbstractEngine {
|
|||
|
||||
@Override
|
||||
public Plan<RenderContext> createFramePlan() {
|
||||
return OnMainThreadPlan.of(this::flushDrawManager);
|
||||
return SyncedPlan.<RenderContext>of(this::flushDrawManager)
|
||||
.then(RaisePlan.raise(flushFlag));
|
||||
}
|
||||
|
||||
private void flushDrawManager() {
|
||||
|
@ -43,18 +48,23 @@ public class IndirectEngine extends AbstractEngine {
|
|||
|
||||
@Override
|
||||
public void renderStage(TaskExecutor executor, RenderContext context, RenderStage stage) {
|
||||
if (!drawManager.hasStage(stage)) {
|
||||
return;
|
||||
if (drawManager.hasStage(stage)) {
|
||||
executor.syncTo(flushFlag);
|
||||
|
||||
try (var restoreState = GlStateTracker.getRestoreState()) {
|
||||
setup();
|
||||
|
||||
for (var list : drawManager.renderLists.values()) {
|
||||
list.submit(stage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
executor.syncPoint();
|
||||
|
||||
try (var restoreState = GlStateTracker.getRestoreState()) {
|
||||
setup();
|
||||
|
||||
for (var list : drawManager.renderLists.values()) {
|
||||
list.submit(stage);
|
||||
}
|
||||
if (stage.isLast()) {
|
||||
// Need to sync here to ensure this frame has everything executed
|
||||
// in case we didn't have any stages to draw this frame.
|
||||
executor.syncTo(flushFlag);
|
||||
executor.lower(flushFlag);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,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.Flag;
|
||||
import com.jozufozu.flywheel.api.task.Plan;
|
||||
import com.jozufozu.flywheel.api.task.TaskExecutor;
|
||||
import com.jozufozu.flywheel.backend.compile.InstancingPrograms;
|
||||
|
@ -17,7 +18,9 @@ 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.OnMainThreadPlan;
|
||||
import com.jozufozu.flywheel.lib.task.NamedFlag;
|
||||
import com.jozufozu.flywheel.lib.task.RaisePlan;
|
||||
import com.jozufozu.flywheel.lib.task.SyncedPlan;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
|
||||
import net.minecraft.client.Minecraft;
|
||||
|
@ -26,6 +29,8 @@ public class InstancingEngine extends AbstractEngine {
|
|||
private final Context context;
|
||||
private final InstancedDrawManager drawManager = new InstancedDrawManager();
|
||||
|
||||
private final Flag flushFlag = new NamedFlag("flushed");
|
||||
|
||||
public InstancingEngine(int maxOriginDistance, Context context) {
|
||||
super(maxOriginDistance);
|
||||
this.context = context;
|
||||
|
@ -38,7 +43,8 @@ public class InstancingEngine extends AbstractEngine {
|
|||
|
||||
@Override
|
||||
public Plan<RenderContext> createFramePlan() {
|
||||
return OnMainThreadPlan.of(this::flushDrawManager);
|
||||
return SyncedPlan.<RenderContext>of(this::flushDrawManager)
|
||||
.then(RaisePlan.raise(flushFlag));
|
||||
}
|
||||
|
||||
private void flushDrawManager() {
|
||||
|
@ -51,16 +57,21 @@ public class InstancingEngine extends AbstractEngine {
|
|||
public void renderStage(TaskExecutor executor, RenderContext context, RenderStage stage) {
|
||||
var drawSet = drawManager.get(stage);
|
||||
|
||||
if (drawSet.isEmpty()) {
|
||||
return;
|
||||
if (!drawSet.isEmpty()) {
|
||||
executor.syncTo(flushFlag);
|
||||
|
||||
try (var state = GlStateTracker.getRestoreState()) {
|
||||
setup();
|
||||
|
||||
render(drawSet);
|
||||
}
|
||||
}
|
||||
|
||||
executor.syncPoint();
|
||||
|
||||
try (var state = GlStateTracker.getRestoreState()) {
|
||||
setup();
|
||||
|
||||
render(drawSet);
|
||||
if (stage.isLast()) {
|
||||
// Need to sync here to ensure this frame has everything executed
|
||||
// in case we didn't have any stages to draw this frame.
|
||||
executor.syncTo(flushFlag);
|
||||
executor.lower(flushFlag);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package com.jozufozu.flywheel.impl.task;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
@ -12,10 +14,12 @@ import org.jetbrains.annotations.NotNull;
|
|||
import org.slf4j.Logger;
|
||||
|
||||
import com.jozufozu.flywheel.Flywheel;
|
||||
import com.jozufozu.flywheel.api.task.Flag;
|
||||
import com.jozufozu.flywheel.api.task.TaskExecutor;
|
||||
import com.mojang.blaze3d.systems.RenderSystem;
|
||||
import com.mojang.logging.LogUtils;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
|
||||
import net.minecraft.util.Mth;
|
||||
|
||||
// https://github.com/CaffeineMC/sodium-fabric/blob/5d364ed5ba63f9067fcf72a078ca310bff4db3e9/src/main/java/me/jellysquid/mods/sodium/client/render/chunk/compile/ChunkBuilder.java
|
||||
|
@ -35,6 +39,8 @@ public class ParallelTaskExecutor implements TaskExecutor {
|
|||
private final Deque<Runnable> taskQueue = new ConcurrentLinkedDeque<>();
|
||||
private final Queue<Runnable> mainThreadQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
private final Set<Flag> flags = Collections.synchronizedSet(new ReferenceOpenHashSet<>());
|
||||
|
||||
private final ThreadGroupNotifier taskNotifier = new ThreadGroupNotifier();
|
||||
private final WaitGroup waitGroup = new WaitGroup();
|
||||
|
||||
|
@ -116,7 +122,7 @@ public class ParallelTaskExecutor implements TaskExecutor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void scheduleForMainThread(Runnable runnable) {
|
||||
public void scheduleForSync(Runnable runnable) {
|
||||
if (!running.get()) {
|
||||
throw new IllegalStateException("Executor is stopped");
|
||||
}
|
||||
|
@ -133,40 +139,67 @@ public class ParallelTaskExecutor implements TaskExecutor {
|
|||
*/
|
||||
@Override
|
||||
public void syncPoint() {
|
||||
Runnable task;
|
||||
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.
|
||||
boolean done = waitGroup.await(10_000);
|
||||
// If we timed-out tasks may have been added to the queue, so check again.
|
||||
if (done && mainThreadQueue.isEmpty()) {
|
||||
// if they didn't, we're done.
|
||||
break;
|
||||
}
|
||||
if (syncOneTask()) {
|
||||
// Done! Nothing left to do.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void discardAndAwait() {
|
||||
@Override
|
||||
public boolean syncTo(Flag flag) {
|
||||
while (true) {
|
||||
// Discard everyone else's work...
|
||||
while (taskQueue.pollLast() != null) {
|
||||
waitGroup.done();
|
||||
if (isRaised(flag)) {
|
||||
// The flag is already raised!
|
||||
// Early return with true to indicate.
|
||||
return true;
|
||||
}
|
||||
|
||||
// ...wait for any stragglers...
|
||||
if (waitGroup.await(100_000)) {
|
||||
break;
|
||||
if (syncOneTask()) {
|
||||
// Out of tasks entirely.
|
||||
// The flag may have been raised though so return the result of isRaised.
|
||||
return isRaised(flag);
|
||||
}
|
||||
}
|
||||
// ...and clear the main thread queue.
|
||||
mainThreadQueue.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to process a single task.
|
||||
*
|
||||
* @return {@code true} if the executor has nothing left to do.
|
||||
*/
|
||||
private boolean syncOneTask() {
|
||||
Runnable task;
|
||||
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.
|
||||
boolean done = waitGroup.await(10_000);
|
||||
// If we timed-out tasks may have been added to the queue, so check again.
|
||||
// if they didn't, we're done.
|
||||
return done && mainThreadQueue.isEmpty();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void raise(Flag flag) {
|
||||
flags.add(flag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lower(Flag flag) {
|
||||
flags.remove(flag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRaised(Flag flag) {
|
||||
return flags.contains(flag);
|
||||
}
|
||||
|
||||
private void processTask(Runnable task) {
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
package com.jozufozu.flywheel.impl.task;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import com.jozufozu.flywheel.api.task.Flag;
|
||||
import com.jozufozu.flywheel.api.task.TaskExecutor;
|
||||
|
||||
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
|
||||
|
||||
public class SerialTaskExecutor implements TaskExecutor {
|
||||
public static final SerialTaskExecutor INSTANCE = new SerialTaskExecutor();
|
||||
|
||||
private final Set<Flag> flags = new ReferenceOpenHashSet<>();
|
||||
|
||||
private SerialTaskExecutor() {
|
||||
}
|
||||
|
||||
|
@ -14,7 +21,7 @@ public class SerialTaskExecutor implements TaskExecutor {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void scheduleForMainThread(Runnable runnable) {
|
||||
public void scheduleForSync(Runnable runnable) {
|
||||
runnable.run();
|
||||
}
|
||||
|
||||
|
@ -22,6 +29,26 @@ public class SerialTaskExecutor implements TaskExecutor {
|
|||
public void syncPoint() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean syncTo(Flag flag) {
|
||||
return isRaised(flag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void raise(Flag flag) {
|
||||
flags.add(flag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lower(Flag flag) {
|
||||
flags.remove(flag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRaised(Flag flag) {
|
||||
return flags.contains(flag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getThreadCount() {
|
||||
return 1;
|
||||
|
|
|
@ -7,6 +7,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.Flag;
|
||||
import com.jozufozu.flywheel.api.task.Plan;
|
||||
import com.jozufozu.flywheel.api.task.TaskExecutor;
|
||||
import com.jozufozu.flywheel.api.visual.DynamicVisual;
|
||||
|
@ -22,7 +23,9 @@ import com.jozufozu.flywheel.impl.visualization.manager.BlockEntityVisualManager
|
|||
import com.jozufozu.flywheel.impl.visualization.manager.EffectVisualManager;
|
||||
import com.jozufozu.flywheel.impl.visualization.manager.EntityVisualManager;
|
||||
import com.jozufozu.flywheel.lib.math.MatrixUtil;
|
||||
import com.jozufozu.flywheel.lib.task.NamedFlag;
|
||||
import com.jozufozu.flywheel.lib.task.NestedPlan;
|
||||
import com.jozufozu.flywheel.lib.task.RaisePlan;
|
||||
import com.jozufozu.flywheel.lib.task.SimplyComposedPlan;
|
||||
import com.jozufozu.flywheel.lib.util.LevelAttached;
|
||||
|
||||
|
@ -49,6 +52,9 @@ public class VisualizationManagerImpl implements VisualizationManager {
|
|||
private final Plan<TickContext> tickPlan;
|
||||
private final Plan<RenderContext> framePlan;
|
||||
|
||||
private final Flag tickFlag = new NamedFlag("tick");
|
||||
private final Flag frameFlag = new NamedFlag("frame");
|
||||
|
||||
private VisualizationManagerImpl(LevelAccessor level) {
|
||||
engine = BackendManager.getBackend()
|
||||
.createEngine(level);
|
||||
|
@ -62,8 +68,9 @@ public class VisualizationManagerImpl implements VisualizationManager {
|
|||
tickPlan = blockEntities.createTickPlan()
|
||||
.and(entities.createTickPlan())
|
||||
.and(effects.createTickPlan())
|
||||
.then(RaisePlan.raise(tickFlag))
|
||||
.simplify();
|
||||
framePlan = new FramePlan();
|
||||
framePlan = new FramePlan().then(RaisePlan.raise(frameFlag));
|
||||
}
|
||||
|
||||
public static boolean supportsVisualization(@Nullable LevelAccessor level) {
|
||||
|
@ -145,7 +152,12 @@ public class VisualizationManagerImpl implements VisualizationManager {
|
|||
* </p>
|
||||
*/
|
||||
public void tick(double cameraX, double cameraY, double cameraZ) {
|
||||
taskExecutor.syncPoint();
|
||||
// Make sure we're done with any prior frame or tick to avoid racing.
|
||||
taskExecutor.syncTo(frameFlag);
|
||||
taskExecutor.lower(frameFlag);
|
||||
|
||||
taskExecutor.syncTo(tickFlag);
|
||||
taskExecutor.lower(tickFlag);
|
||||
|
||||
tickPlan.execute(taskExecutor, new TickContext(cameraX, cameraY, cameraZ));
|
||||
}
|
||||
|
@ -159,7 +171,9 @@ public class VisualizationManagerImpl implements VisualizationManager {
|
|||
* </p>
|
||||
*/
|
||||
public void beginFrame(RenderContext context) {
|
||||
taskExecutor.syncPoint();
|
||||
// Make sure we're done with the last tick.
|
||||
// Note we don't lower here because many frames may happen per tick.
|
||||
taskExecutor.syncTo(tickFlag);
|
||||
|
||||
framePlan.execute(taskExecutor, context);
|
||||
}
|
||||
|
@ -175,7 +189,10 @@ public class VisualizationManagerImpl implements VisualizationManager {
|
|||
* Free all acquired resources and delete this manager.
|
||||
*/
|
||||
public void delete() {
|
||||
taskExecutor.discardAndAwait();
|
||||
// Just finish everything. This may include the work of others but that's okay.
|
||||
taskExecutor.syncPoint();
|
||||
|
||||
// Now clean up.
|
||||
blockEntities.invalidate();
|
||||
entities.invalidate();
|
||||
effects.invalidate();
|
||||
|
|
11
src/main/java/com/jozufozu/flywheel/lib/task/NamedFlag.java
Normal file
11
src/main/java/com/jozufozu/flywheel/lib/task/NamedFlag.java
Normal file
|
@ -0,0 +1,11 @@
|
|||
package com.jozufozu.flywheel.lib.task;
|
||||
|
||||
import com.jozufozu.flywheel.api.task.Flag;
|
||||
|
||||
/**
|
||||
* A flag with an arbitrary name.
|
||||
*
|
||||
* @param name The name of the flag, mainly for debugging purposes.
|
||||
*/
|
||||
public record NamedFlag(String name) implements Flag {
|
||||
}
|
16
src/main/java/com/jozufozu/flywheel/lib/task/RaisePlan.java
Normal file
16
src/main/java/com/jozufozu/flywheel/lib/task/RaisePlan.java
Normal file
|
@ -0,0 +1,16 @@
|
|||
package com.jozufozu.flywheel.lib.task;
|
||||
|
||||
import com.jozufozu.flywheel.api.task.Flag;
|
||||
import com.jozufozu.flywheel.api.task.TaskExecutor;
|
||||
|
||||
public record RaisePlan<C>(Flag flag) implements SimplyComposedPlan<C> {
|
||||
public static <C> RaisePlan<C> raise(Flag flag) {
|
||||
return new RaisePlan<>(flag);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(TaskExecutor taskExecutor, C context, Runnable onCompletion) {
|
||||
taskExecutor.raise(flag);
|
||||
onCompletion.run();
|
||||
}
|
||||
}
|
12
src/main/java/com/jozufozu/flywheel/lib/task/StageFlag.java
Normal file
12
src/main/java/com/jozufozu/flywheel/lib/task/StageFlag.java
Normal file
|
@ -0,0 +1,12 @@
|
|||
package com.jozufozu.flywheel.lib.task;
|
||||
|
||||
import com.jozufozu.flywheel.api.event.RenderStage;
|
||||
import com.jozufozu.flywheel.api.task.Flag;
|
||||
|
||||
/**
|
||||
* A flag that is associated with a render stage.
|
||||
* <br>
|
||||
* Useful for synchronizing tasks for a specific render stage.
|
||||
*/
|
||||
public record StageFlag(RenderStage stage) implements Flag {
|
||||
}
|
|
@ -3,18 +3,18 @@ package com.jozufozu.flywheel.lib.task;
|
|||
import com.jozufozu.flywheel.api.task.Plan;
|
||||
import com.jozufozu.flywheel.api.task.TaskExecutor;
|
||||
|
||||
public record OnMainThreadPlan<C>(ContextConsumer<C> task) implements SimplyComposedPlan<C> {
|
||||
public record SyncedPlan<C>(ContextConsumer<C> task) implements SimplyComposedPlan<C> {
|
||||
public static <C> Plan<C> of(ContextConsumer<C> task) {
|
||||
return new OnMainThreadPlan<>(task);
|
||||
return new SyncedPlan<>(task);
|
||||
}
|
||||
|
||||
public static <C> Plan<C> of(ContextRunnable<C> task) {
|
||||
return new OnMainThreadPlan<>(task);
|
||||
return new SyncedPlan<>(task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(TaskExecutor taskExecutor, C context, Runnable onCompletion) {
|
||||
taskExecutor.scheduleForMainThread(() -> {
|
||||
taskExecutor.scheduleForSync(() -> {
|
||||
task.accept(context);
|
||||
onCompletion.run();
|
||||
});
|
|
@ -140,7 +140,7 @@ class PlanExecutionTest {
|
|||
@Test
|
||||
void mainThreadPlan() {
|
||||
var done = new AtomicBoolean(false);
|
||||
var plan = OnMainThreadPlan.of(() -> done.set(true));
|
||||
var plan = SyncedPlan.of(() -> done.set(true));
|
||||
|
||||
plan.execute(EXECUTOR, Unit.INSTANCE);
|
||||
|
||||
|
@ -151,6 +151,95 @@ class PlanExecutionTest {
|
|||
Assertions.assertTrue(done.get());
|
||||
}
|
||||
|
||||
@Test
|
||||
void flagPlan() {
|
||||
var first = new NamedFlag("ready right away");
|
||||
var second = new NamedFlag("ready after we sync");
|
||||
|
||||
var sync = new Synchronizer(2, () -> EXECUTOR.raise(second));
|
||||
|
||||
RaisePlan.raise(first)
|
||||
.execute(EXECUTOR, Unit.INSTANCE, sync);
|
||||
|
||||
Assertions.assertTrue(EXECUTOR.syncTo(first), "First flag should be raised since we submitted a plan that raises it");
|
||||
|
||||
Assertions.assertFalse(EXECUTOR.syncTo(second), "Second flag should not be raised yet.");
|
||||
|
||||
sync.decrementAndEventuallyRun();
|
||||
|
||||
Assertions.assertTrue(EXECUTOR.syncTo(second), "Second flag should be raised since it was raised in sync.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void longWaitForFlag() {
|
||||
var first = new NamedFlag("ready right away");
|
||||
var second = new NamedFlag("ready after 2s");
|
||||
|
||||
RaisePlan.raise(first)
|
||||
.then(SimplePlan.of(() -> {
|
||||
try {
|
||||
Thread.sleep(2000);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}))
|
||||
.then(RaisePlan.raise(second))
|
||||
.execute(EXECUTOR, Unit.INSTANCE);
|
||||
|
||||
Assertions.assertTrue(EXECUTOR.syncTo(first), "First flag should be raised since we submitted a plan that raises it.");
|
||||
|
||||
Assertions.assertFalse(EXECUTOR.isRaised(second), "Second flag should not be raised immediately.");
|
||||
|
||||
Assertions.assertTrue(EXECUTOR.syncTo(second), "Second flag should be raised since we were waiting for it.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void syncToReturnsExpected() {
|
||||
var flag = new NamedFlag("ready right away");
|
||||
|
||||
Assertions.assertFalse(EXECUTOR.syncTo(flag), "Flag should not be raised yet.");
|
||||
|
||||
EXECUTOR.raise(flag);
|
||||
|
||||
Assertions.assertTrue(EXECUTOR.syncTo(flag), "Flag should be raised since we raised it manually.");
|
||||
|
||||
EXECUTOR.lower(flag);
|
||||
|
||||
Assertions.assertFalse(EXECUTOR.syncTo(flag), "Flag should not be raised since we lowered it.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void flagsAreReferenceEqual() {
|
||||
var flagA = new NamedFlag("same");
|
||||
var flagB = new NamedFlag("same");
|
||||
|
||||
Assertions.assertNotSame(flagA, flagB, "Flags should not be the same object.");
|
||||
Assertions.assertEquals(flagA, flagB, "Flags should be equal.");
|
||||
|
||||
Assertions.assertFalse(EXECUTOR.isRaised(flagA), "Flag A should not be raised yet.");
|
||||
Assertions.assertFalse(EXECUTOR.isRaised(flagB), "Flag B should not be raised yet.");
|
||||
|
||||
EXECUTOR.raise(flagA);
|
||||
|
||||
Assertions.assertTrue(EXECUTOR.isRaised(flagA), "Flag A should be raised since we raised it manually.");
|
||||
Assertions.assertFalse(EXECUTOR.isRaised(flagB), "Flag B should not be raised yet.");
|
||||
|
||||
EXECUTOR.raise(flagB);
|
||||
|
||||
Assertions.assertTrue(EXECUTOR.isRaised(flagA), "Flag A should be raised since we raised it manually.");
|
||||
Assertions.assertTrue(EXECUTOR.isRaised(flagB), "Flag B should be raised since we raised it manually.");
|
||||
|
||||
EXECUTOR.lower(flagA);
|
||||
|
||||
Assertions.assertFalse(EXECUTOR.isRaised(flagA), "Flag A should not be raised since we lowered it.");
|
||||
Assertions.assertTrue(EXECUTOR.isRaised(flagB), "Flag B should be raised since we raised it manually.");
|
||||
|
||||
EXECUTOR.lower(flagB);
|
||||
|
||||
Assertions.assertFalse(EXECUTOR.isRaised(flagA), "Flag A should not be raised since we lowered it.");
|
||||
Assertions.assertFalse(EXECUTOR.isRaised(flagB), "Flag B should not be raised since we lowered it.");
|
||||
}
|
||||
|
||||
private static void assertExpectedSequence(IntArrayList sequence, int... expected) {
|
||||
Assertions.assertArrayEquals(expected, sequence.toIntArray());
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ public class PlanSimplificationTest {
|
|||
|
||||
Assertions.assertEquals(oneSimple.simplify(), SIMPLE);
|
||||
|
||||
var mainThreadNoop = new OnMainThreadPlan<>(NOOP);
|
||||
var mainThreadNoop = new SyncedPlan<>(NOOP);
|
||||
var oneMainThread = NestedPlan.of(mainThreadNoop);
|
||||
|
||||
Assertions.assertEquals(oneMainThread.simplify(), mainThreadNoop);
|
||||
|
@ -66,7 +66,7 @@ public class PlanSimplificationTest {
|
|||
|
||||
@Test
|
||||
void complexNesting() {
|
||||
var mainThreadNoop = OnMainThreadPlan.<Unit>of(() -> {
|
||||
var mainThreadNoop = SyncedPlan.<Unit>of(() -> {
|
||||
});
|
||||
|
||||
var nested = NestedPlan.of(mainThreadNoop, SIMPLE);
|
||||
|
@ -79,7 +79,7 @@ public class PlanSimplificationTest {
|
|||
|
||||
@Test
|
||||
void nestedNoSimple() {
|
||||
var mainThreadNoop = OnMainThreadPlan.<Unit>of(() -> {
|
||||
var mainThreadNoop = SyncedPlan.<Unit>of(() -> {
|
||||
});
|
||||
var barrier = new BarrierPlan<>(SIMPLE, SIMPLE);
|
||||
var oneMainThread = NestedPlan.of(mainThreadNoop, NestedPlan.of(mainThreadNoop, barrier, barrier));
|
||||
|
|
Loading…
Reference in a new issue