From bc9f156cfdab6ea13b0fd6b6a037934c9ed5c4cb Mon Sep 17 00:00:00 2001 From: Jozufozu Date: Sun, 21 Jan 2024 21:28:41 -0800 Subject: [PATCH] Light refactoring - Run light updates in parallel to the visual frame plans - Add null check in AbstractVisual#relight - Add AbstractVisual#relight taking an iterable. - Begin frame is after light updates are complete. I tried dividing some work to run before, but it doesn't really make a difference. - Remove LightUpdatedLevel. - Remove FrameContext. - LightUpdater no longer runs ticks. - LightUpdater no longer stores things weakly. - Fix some docs. --- .../jozufozu/flywheel/api/visual/Visual.java | 16 ++-- .../impl/visualization/FrameContext.java | 24 ------ .../VisualizationManagerImpl.java | 50 +++++++---- .../manager/VisualManagerImpl.java | 8 +- .../flywheel/lib/light/LightUpdatedLevel.java | 18 ---- .../flywheel/lib/light/LightUpdaterImpl.java | 85 +++++++++---------- .../lib/light/TickingLightListener.java | 1 + .../flywheel/lib/visual/AbstractVisual.java | 23 ++++- .../flywheel/mixin/LevelRendererMixin.java | 8 +- 9 files changed, 104 insertions(+), 129 deletions(-) delete mode 100644 src/main/java/com/jozufozu/flywheel/impl/visualization/FrameContext.java delete mode 100644 src/main/java/com/jozufozu/flywheel/lib/light/LightUpdatedLevel.java diff --git a/src/main/java/com/jozufozu/flywheel/api/visual/Visual.java b/src/main/java/com/jozufozu/flywheel/api/visual/Visual.java index 52233438e..87c392315 100644 --- a/src/main/java/com/jozufozu/flywheel/api/visual/Visual.java +++ b/src/main/java/com/jozufozu/flywheel/api/visual/Visual.java @@ -14,20 +14,18 @@ public interface Visual { /** * Update instances here. Good for when instances don't change very often and when animations are GPU based. - * - *

If your animations are complex or more CPU driven, see {@link DynamicVisual} or {@link TickableVisual}. + *
+ *
If your animations are complex or more CPU driven, see {@link DynamicVisual} or {@link TickableVisual}. */ void update(float partialTick); /** * When a visual is reset, the visual is deleted and re-created. - * - *

- * Just before {@link #update()} would be called, {@code shouldReset()} is checked. - * If this function returns {@code true}, then this visual will be {@link #delete deleted}, - * and another visual will be constructed to replace it. This allows for more sane resource - * acquisition compared to trying to update everything within the lifetime of a visual. - *

+ *
+ * Just before {@link #update)} would be called, {@code shouldReset} is checked. + * If this function returns {@code true}, then this visual will be {@link #delete deleted}, + * and another visual will be constructed to replace it. This allows for more sane resource + * acquisition compared to trying to update everything within the lifetime of a visual. * * @return {@code true} if this visual should be discarded and refreshed. */ diff --git a/src/main/java/com/jozufozu/flywheel/impl/visualization/FrameContext.java b/src/main/java/com/jozufozu/flywheel/impl/visualization/FrameContext.java deleted file mode 100644 index 7da4537ed..000000000 --- a/src/main/java/com/jozufozu/flywheel/impl/visualization/FrameContext.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.jozufozu.flywheel.impl.visualization; - -import org.joml.FrustumIntersection; -import org.joml.Matrix4f; - -import com.jozufozu.flywheel.api.event.RenderContext; - -import net.minecraft.core.Vec3i; - -public record FrameContext(double cameraX, double cameraY, double cameraZ, FrustumIntersection frustum, float partialTick) { - public static FrameContext create(RenderContext context, Vec3i renderOrigin) { - var cameraPos = context.camera() - .getPosition(); - double cameraX = cameraPos.x; - double cameraY = cameraPos.y; - double cameraZ = cameraPos.z; - - Matrix4f viewProjection = new Matrix4f(context.viewProjection()); - viewProjection.translate((float) (renderOrigin.getX() - cameraX), (float) (renderOrigin.getY() - cameraY), (float) (renderOrigin.getZ() - cameraZ)); - FrustumIntersection frustum = new FrustumIntersection(viewProjection); - - return new FrameContext(cameraX, cameraY, cameraZ, frustum, context.partialTick()); - } -} diff --git a/src/main/java/com/jozufozu/flywheel/impl/visualization/VisualizationManagerImpl.java b/src/main/java/com/jozufozu/flywheel/impl/visualization/VisualizationManagerImpl.java index 9aa56c67a..009fcf32b 100644 --- a/src/main/java/com/jozufozu/flywheel/impl/visualization/VisualizationManagerImpl.java +++ b/src/main/java/com/jozufozu/flywheel/impl/visualization/VisualizationManagerImpl.java @@ -6,6 +6,8 @@ import java.util.SortedSet; import java.util.function.Supplier; import org.jetbrains.annotations.Nullable; +import org.joml.FrustumIntersection; +import org.joml.Matrix4f; import com.jozufozu.flywheel.api.backend.BackendManager; import com.jozufozu.flywheel.api.backend.Engine; @@ -100,21 +102,25 @@ public class VisualizationManagerImpl implements VisualizationManager, Supplier< .then(RaisePlan.raise(tickFlag)) .simplify(); + var lightUpdatePlan = lightUpdater.plan(); + + var recreate = SimplePlan.of(context -> blockEntities.getStorage() + .recreateAll(context.partialTick()), context -> entities.getStorage() + .recreateAll(context.partialTick()), context -> effects.getStorage() + .recreateAll(context.partialTick())) + .then(lightUpdatePlan); + + var update = SimplePlan.of(context -> blockEntities.processQueue(context.partialTick()), context -> entities.processQueue(context.partialTick()), context -> effects.processQueue(context.partialTick())) + .then(lightUpdatePlan.and(MapContextPlan.map(this::createVisualContext) + .to(NestedPlan.of(blockEntities.getStorage() + .getFramePlan(), entities.getStorage() + .getFramePlan(), effects.getStorage() + .getFramePlan())))); + framePlan = IfElsePlan.on((RenderContext ctx) -> engine.updateRenderOrigin(ctx.camera())) - .ifTrue(MapContextPlan.map(RenderContext::partialTick) - .to(blockEntities.createRecreationPlan() - .and(entities.createRecreationPlan()) - .and(effects.createRecreationPlan()))) - .ifFalse(MapContextPlan.map((RenderContext ctx) -> createVisualContext(FrameContext.create(ctx, engine.renderOrigin()))) - .to(NestedPlan.of(SimplePlan.of(context -> blockEntities.processQueue(context.partialTick())) - .then(blockEntities.getStorage() - .getFramePlan()), SimplePlan.of(context -> entities.processQueue(context.partialTick())) - .then(entities.getStorage() - .getFramePlan()), SimplePlan.of(context -> effects.processQueue(context.partialTick())) - .then(effects.getStorage() - .getFramePlan())))) + .ifTrue(recreate) + .ifFalse(update) .plan() - .then(lightUpdater.plan()) .then(RaisePlan.raise(frameVisualsFlag)) .then(engine.createFramePlan()) .then(RaisePlan.raise(frameFlag)) @@ -126,8 +132,19 @@ public class VisualizationManagerImpl implements VisualizationManager, Supplier< } } - private VisualFrameContext createVisualContext(FrameContext ctx) { - return new VisualFrameContext(ctx.cameraX(), ctx.cameraY(), ctx.cameraZ(), ctx.frustum(), ctx.partialTick(), frameLimiter); + private VisualFrameContext createVisualContext(RenderContext ctx) { + Vec3i renderOrigin = engine.renderOrigin(); + var cameraPos = ctx.camera() + .getPosition(); + double cameraX = cameraPos.x; + double cameraY = cameraPos.y; + double cameraZ = cameraPos.z; + + Matrix4f viewProjection = new Matrix4f(ctx.viewProjection()); + viewProjection.translate((float) (renderOrigin.getX() - cameraX), (float) (renderOrigin.getY() - cameraY), (float) (renderOrigin.getZ() - cameraZ)); + FrustumIntersection frustum = new FrustumIntersection(viewProjection); + + return new VisualFrameContext(cameraX, cameraY, cameraZ, frustum, ctx.partialTick(), frameLimiter); } private VisualTickContext createVisualTickContext(TickContext ctx) { @@ -235,8 +252,6 @@ public class VisualizationManagerImpl implements VisualizationManager, Supplier< tickLimiter.tick(); - lightUpdater.tick(); - tickPlan.execute(taskExecutor, new TickContext(cameraX, cameraY, cameraZ)); } @@ -257,6 +272,7 @@ public class VisualizationManagerImpl implements VisualizationManager, Supplier< frameFlag.lower(); frameLimiter.tick(); + framePlan.execute(taskExecutor, context); } diff --git a/src/main/java/com/jozufozu/flywheel/impl/visualization/manager/VisualManagerImpl.java b/src/main/java/com/jozufozu/flywheel/impl/visualization/manager/VisualManagerImpl.java index aa9cd99f8..42f122ba7 100644 --- a/src/main/java/com/jozufozu/flywheel/impl/visualization/manager/VisualManagerImpl.java +++ b/src/main/java/com/jozufozu/flywheel/impl/visualization/manager/VisualManagerImpl.java @@ -3,16 +3,14 @@ package com.jozufozu.flywheel.impl.visualization.manager; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; -import com.jozufozu.flywheel.api.task.Plan; import com.jozufozu.flywheel.api.visualization.VisualManager; import com.jozufozu.flywheel.impl.visualization.storage.Storage; import com.jozufozu.flywheel.impl.visualization.storage.Transaction; -import com.jozufozu.flywheel.lib.task.SimplePlan; public class VisualManagerImpl> implements VisualManager { private final Queue> queue = new ConcurrentLinkedQueue<>(); - protected final S storage; + private final S storage; public VisualManagerImpl(S storage) { this.storage = storage; @@ -50,10 +48,6 @@ public class VisualManagerImpl> implements VisualManager queue.add(Transaction.update(obj)); } - public Plan createRecreationPlan() { - return SimplePlan.of(getStorage()::recreateAll); - } - public void invalidate() { getStorage().invalidate(); } diff --git a/src/main/java/com/jozufozu/flywheel/lib/light/LightUpdatedLevel.java b/src/main/java/com/jozufozu/flywheel/lib/light/LightUpdatedLevel.java deleted file mode 100644 index d2b8d952a..000000000 --- a/src/main/java/com/jozufozu/flywheel/lib/light/LightUpdatedLevel.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.jozufozu.flywheel.lib.light; - -import net.minecraft.world.level.LevelAccessor; - -/** - * Marker interface for custom/fake levels to indicate that LightUpdater should interact with it.

- * - * Implement this if your custom level has light updates at all. If so, be sure to call - * {@link com.jozufozu.flywheel.lib.util.LevelAttached#invalidateLevel} when your level is unloaded. - */ -public interface LightUpdatedLevel extends LevelAccessor { - /** - * @return {@code true} if this level is passing light updates into LightUpdater. - */ - default boolean receivesLightUpdates() { - return true; - } -} diff --git a/src/main/java/com/jozufozu/flywheel/lib/light/LightUpdaterImpl.java b/src/main/java/com/jozufozu/flywheel/lib/light/LightUpdaterImpl.java index 5813fd45a..a866a38cc 100644 --- a/src/main/java/com/jozufozu/flywheel/lib/light/LightUpdaterImpl.java +++ b/src/main/java/com/jozufozu/flywheel/lib/light/LightUpdaterImpl.java @@ -1,21 +1,24 @@ package com.jozufozu.flywheel.lib.light; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Queue; -import java.util.Set; import java.util.WeakHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.stream.Stream; import com.jozufozu.flywheel.api.event.RenderContext; import com.jozufozu.flywheel.api.task.Plan; +import com.jozufozu.flywheel.api.task.TaskExecutor; import com.jozufozu.flywheel.api.visualization.LightUpdater; import com.jozufozu.flywheel.lib.box.Box; -import com.jozufozu.flywheel.lib.task.ForEachPlan; -import com.jozufozu.flywheel.lib.task.IfElsePlan; -import com.jozufozu.flywheel.lib.task.SimplePlan; -import com.jozufozu.flywheel.lib.util.FlwUtil; +import com.jozufozu.flywheel.lib.task.PlanUtil; +import com.jozufozu.flywheel.lib.task.SimplyComposedPlan; +import com.jozufozu.flywheel.lib.task.Synchronizer; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import it.unimi.dsi.fastutil.longs.LongArraySet; import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import it.unimi.dsi.fastutil.longs.LongSet; @@ -24,13 +27,10 @@ import net.minecraft.world.level.LightLayer; /** * Keeps track of what chunks/sections each listener is in, so we can update exactly what needs to be updated. - * - * @apiNote Custom/fake levels (that are {@code != Minecraft.getInstance.level}) need to implement - * {@link LightUpdatedLevel} for LightUpdater to work with them. */ public class LightUpdaterImpl implements LightUpdater { - private final WeakHashMap listenersAndTheirSections = new WeakHashMap<>(); - private final Set tickingListeners = FlwUtil.createWeakHashSet(); + private final Map listenersAndTheirSections = new WeakHashMap<>(); + private final Long2ObjectMap> listenersBySection = new Long2ObjectOpenHashMap<>(); private final Queue additionQueue = new ConcurrentLinkedQueue<>(); private final LongSet sectionsQueue = new LongOpenHashSet(); @@ -49,30 +49,32 @@ public class LightUpdaterImpl implements LightUpdater { } public Plan plan() { - // Assume we'll have more listeners than sections updated - // TODO: this is slow, maybe launch a task for each changed section and distribute from there? - return SimplePlan.of(this::processQueue) - .then(IfElsePlan.on(() -> !sectionsQueue.isEmpty()) - .ifTrue(ForEachPlan.of(() -> listenersAndTheirSections.entrySet() - .stream() - .toList(), this::updateOneEntry)) - .plan()) - .then(SimplePlan.of(() -> sectionsQueue.clear())); - } + return (SimplyComposedPlan) (TaskExecutor taskExecutor, RenderContext context, Runnable onCompletion) -> { + processQueue(); - private void updateOneEntry(Map.Entry entry) { - - updateOne(entry.getKey(), entry.getValue()); - - } - - private void updateOne(LightListener listener, LongSet containedSections) { - for (long l : containedSections.toLongArray()) { - if (sectionsQueue.contains(l)) { - listener.onLightUpdate(LightLayer.BLOCK, SectionPos.of(l)); - break; + if (sectionsQueue.isEmpty()) { + onCompletion.run(); + return; } - } + + var sync = new Synchronizer(sectionsQueue.size(), () -> { + sectionsQueue.clear(); + onCompletion.run(); + }); + + sectionsQueue.forEach((long section) -> { + List listeners = listenersBySection.get(section); + if (listeners != null && !listeners.isEmpty()) { + taskExecutor.execute(() -> { + PlanUtil.distribute(taskExecutor, SectionPos.of(section), sync, listeners, (listener, pos) -> { + listener.onLightUpdate(LightLayer.BLOCK, pos); + }); + }); + } else { + sync.decrementAndEventuallyRun(); + } + }); + }; } public Stream getAllBoxes() { @@ -85,16 +87,6 @@ public class LightUpdaterImpl implements LightUpdater { return listenersAndTheirSections.isEmpty(); } - public void tick() { - processQueue(); - - for (TickingLightListener tickingListener : tickingListeners) { - if (tickingListener.tickLightListener()) { - addListener(tickingListener); - } - } - } - private synchronized void processQueue() { LightListener listener; while ((listener = additionQueue.poll()) != null) { @@ -103,10 +95,6 @@ public class LightUpdaterImpl implements LightUpdater { } private void doAdd(LightListener listener) { - if (listener instanceof TickingLightListener ticking) { - tickingListeners.add(ticking); - } - Box box = listener.getVolume(); LongSet sections = new LongArraySet(); @@ -121,7 +109,10 @@ public class LightUpdaterImpl implements LightUpdater { for (int x = minX; x <= maxX; x++) { for (int y = minY; y <= maxY; y++) { for (int z = minZ; z <= maxZ; z++) { - sections.add(SectionPos.asLong(x, y, z)); + var longPos = SectionPos.asLong(x, y, z); + sections.add(longPos); + listenersBySection.computeIfAbsent(longPos, $ -> new ArrayList<>()) + .add(listener); } } } diff --git a/src/main/java/com/jozufozu/flywheel/lib/light/TickingLightListener.java b/src/main/java/com/jozufozu/flywheel/lib/light/TickingLightListener.java index 64fdf3b13..28530f1a9 100644 --- a/src/main/java/com/jozufozu/flywheel/lib/light/TickingLightListener.java +++ b/src/main/java/com/jozufozu/flywheel/lib/light/TickingLightListener.java @@ -1,5 +1,6 @@ package com.jozufozu.flywheel.lib.light; +// TODO: remove public interface TickingLightListener extends LightListener { /** * Called every tick for active listeners. diff --git a/src/main/java/com/jozufozu/flywheel/lib/visual/AbstractVisual.java b/src/main/java/com/jozufozu/flywheel/lib/visual/AbstractVisual.java index f54cb2d02..8dc4c8f2f 100644 --- a/src/main/java/com/jozufozu/flywheel/lib/visual/AbstractVisual.java +++ b/src/main/java/com/jozufozu/flywheel/lib/visual/AbstractVisual.java @@ -1,5 +1,6 @@ package com.jozufozu.flywheel.lib.visual; +import java.util.Objects; import java.util.stream.Stream; import com.jozufozu.flywheel.api.instance.InstancerProvider; @@ -88,6 +89,10 @@ public abstract class AbstractVisual implements Visual, LightListener { protected void relight(int block, int sky, FlatLit... instances) { for (FlatLit instance : instances) { + if (instance == null) { + continue; + } + instance.setLight(block, sky); instance.handle() .setChanged(); @@ -99,8 +104,24 @@ public abstract class AbstractVisual implements Visual, LightListener { } protected void relight(int block, int sky, Stream instances) { - instances.forEach(model -> model.setLight(block, sky) + instances.filter(Objects::nonNull) + .forEach(instance -> instance.setLight(block, sky) .handle() .setChanged()); } + + protected void relight(BlockPos pos, Iterable instances) { + relight(level.getBrightness(LightLayer.BLOCK, pos), level.getBrightness(LightLayer.SKY, pos), instances); + } + + protected void relight(int block, int sky, Iterable instances) { + for (FlatLit instance : instances) { + if (instance == null) { + continue; + } + instance.setLight(block, sky) + .handle() + .setChanged(); + } + } } diff --git a/src/main/java/com/jozufozu/flywheel/mixin/LevelRendererMixin.java b/src/main/java/com/jozufozu/flywheel/mixin/LevelRendererMixin.java index 2be9760fe..367d224db 100644 --- a/src/main/java/com/jozufozu/flywheel/mixin/LevelRendererMixin.java +++ b/src/main/java/com/jozufozu/flywheel/mixin/LevelRendererMixin.java @@ -47,13 +47,9 @@ abstract class LevelRendererMixin { @Unique private RenderContext flywheel$renderContext; - @Inject(method = "renderLevel", at = @At("HEAD")) - private void flywheel$beginRender(PoseStack poseStack, float partialTick, long finishNanoTime, boolean renderBlockOutline, Camera camera, GameRenderer gameRenderer, LightTexture lightTexture, Matrix4f projectionMatrix, CallbackInfo ci) { - // TODO: divide some work to here, light updates may take a while - } - + // @Inject(method = "renderLevel", at = @At("HEAD")) @Inject(method = "renderLevel", at = @At(value = "INVOKE_ASSIGN", target = "Lnet/minecraft/world/level/lighting/LevelLightEngine;runLightUpdates()I")) - private void flywheel$processLightUpdates(PoseStack poseStack, float partialTick, long finishNanoTime, boolean renderBlockOutline, Camera camera, GameRenderer gameRenderer, LightTexture lightTexture, Matrix4f projectionMatrix, CallbackInfo ci) { + private void flywheel$beginRender(PoseStack poseStack, float partialTick, long finishNanoTime, boolean renderBlockOutline, Camera camera, GameRenderer gameRenderer, LightTexture lightTexture, Matrix4f projectionMatrix, CallbackInfo ci) { flywheel$renderContext = RenderContext.create((LevelRenderer) (Object) this, level, renderBuffers, poseStack, projectionMatrix, camera, partialTick); MinecraftForge.EVENT_BUS.post(new BeginFrameEvent(flywheel$renderContext));