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.
This commit is contained in:
Jozufozu 2024-01-21 21:28:41 -08:00
parent 77fd9dab1e
commit bc9f156cfd
9 changed files with 104 additions and 129 deletions

View file

@ -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.
*
* <br><br> If your animations are complex or more CPU driven, see {@link DynamicVisual} or {@link TickableVisual}.
* <br>
* <br> 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.
*
* <p>
* 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.
* </p>
* <br>
* 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.
*/

View file

@ -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());
}
}

View file

@ -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.<RenderContext>of(context -> blockEntities.getStorage()
.recreateAll(context.partialTick()), context -> entities.getStorage()
.recreateAll(context.partialTick()), context -> effects.getStorage()
.recreateAll(context.partialTick()))
.then(lightUpdatePlan);
var update = SimplePlan.<RenderContext>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.<VisualFrameContext>of(context -> blockEntities.processQueue(context.partialTick()))
.then(blockEntities.getStorage()
.getFramePlan()), SimplePlan.<VisualFrameContext>of(context -> entities.processQueue(context.partialTick()))
.then(entities.getStorage()
.getFramePlan()), SimplePlan.<VisualFrameContext>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);
}

View file

@ -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<T, S extends Storage<T>> implements VisualManager<T> {
private final Queue<Transaction<T>> 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<T, S extends Storage<T>> implements VisualManager
queue.add(Transaction.update(obj));
}
public Plan<Float> createRecreationPlan() {
return SimplePlan.of(getStorage()::recreateAll);
}
public void invalidate() {
getStorage().invalidate();
}

View file

@ -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.<p>
*
* 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;
}
}

View file

@ -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<LightListener, LongSet> listenersAndTheirSections = new WeakHashMap<>();
private final Set<TickingLightListener> tickingListeners = FlwUtil.createWeakHashSet();
private final Map<LightListener, LongSet> listenersAndTheirSections = new WeakHashMap<>();
private final Long2ObjectMap<List<LightListener>> listenersBySection = new Long2ObjectOpenHashMap<>();
private final Queue<LightListener> additionQueue = new ConcurrentLinkedQueue<>();
private final LongSet sectionsQueue = new LongOpenHashSet();
@ -49,30 +49,32 @@ public class LightUpdaterImpl implements LightUpdater {
}
public Plan<RenderContext> 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.<RenderContext>of(this::processQueue)
.then(IfElsePlan.<RenderContext>on(() -> !sectionsQueue.isEmpty())
.ifTrue(ForEachPlan.of(() -> listenersAndTheirSections.entrySet()
.stream()
.toList(), this::updateOneEntry))
.plan())
.then(SimplePlan.of(() -> sectionsQueue.clear()));
}
return (SimplyComposedPlan<RenderContext>) (TaskExecutor taskExecutor, RenderContext context, Runnable onCompletion) -> {
processQueue();
private void updateOneEntry(Map.Entry<LightListener, LongSet> 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<LightListener> 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<Box> 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);
}
}
}

View file

@ -1,5 +1,6 @@
package com.jozufozu.flywheel.lib.light;
// TODO: remove
public interface TickingLightListener extends LightListener {
/**
* Called every tick for active listeners.

View file

@ -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<? extends FlatLit> 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<? extends FlatLit> instances) {
relight(level.getBrightness(LightLayer.BLOCK, pos), level.getBrightness(LightLayer.SKY, pos), instances);
}
protected void relight(int block, int sky, Iterable<? extends FlatLit> instances) {
for (FlatLit instance : instances) {
if (instance == null) {
continue;
}
instance.setLight(block, sky)
.handle()
.setChanged();
}
}
}

View file

@ -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));