Lit beyond bounds

- Add callback to LitVisual to allow LitVisuals to change sections
- Move visual managers' tick/frame plan creation back into
  VisualManagerImpl.
- Some small reorganization in LitVisualStorage.
This commit is contained in:
Jozufozu 2024-01-25 21:17:36 -08:00
parent e7c27109e5
commit 4698d7b394
6 changed files with 116 additions and 27 deletions

View File

@ -26,10 +26,33 @@ public interface LitVisual extends Visual {
/**
* Collect the sections that this visual is contained in.
* <br>
* This method is only called upon visual creation.
* This method is called upon visual creation, and the frame after {@link Notifier#notifySectionsChanged} is called.
*
* @param consumer The consumer to provide the sections to.
* @see SectionPos#asLong
*/
void collectLightSections(LongConsumer consumer);
/**
* Set the notifier object.
* <br>
* This method is only called once, upon visual creation,
* after {@link #init} and before {@link #collectLightSections}.
*
* @param notifier The notifier.
*/
void initLightSectionNotifier(Notifier notifier);
/**
* A notifier object that can be used to indicate to the impl
* that the sections a visual is contained in have changed.
*/
interface Notifier {
/**
* Invoke this to indicate to the impl that your visual has moved to a different set of sections.
* <br>
* The next frame, the impl will call {@link LitVisual#collectLightSections} again.
*/
void notifySectionsChanged();
}
}

View File

@ -98,20 +98,14 @@ public class VisualizationManagerImpl implements VisualizationManager {
entities = new VisualManagerImpl<>(entitiesStorage);
effects = new VisualManagerImpl<>(effectsStorage);
tickPlan = NestedPlan.of(SimplePlan.<VisualTickContext>of(context -> blockEntities.processQueue(0))
.then(blockEntitiesStorage.getTickPlan()), SimplePlan.<VisualTickContext>of(context -> entities.processQueue(0))
.then(entitiesStorage.getTickPlan()), SimplePlan.<VisualTickContext>of(context -> effects.processQueue(0))
.then(effectsStorage.getTickPlan()))
tickPlan = NestedPlan.of(blockEntities.tickPlan(), entities.tickPlan(), effects.tickPlan())
.then(RaisePlan.raise(tickFlag))
.simplify();
var recreate = SimplePlan.<RenderContext>of(context -> blockEntitiesStorage.recreateAll(context.partialTick()), context -> entitiesStorage.recreateAll(context.partialTick()), context -> effectsStorage.recreateAll(context.partialTick()));
var update = MapContextPlan.map(this::createVisualFrameContext)
.to(NestedPlan.of(SimplePlan.<VisualFrameContext>of(context -> blockEntities.processQueue(0))
.then(blockEntitiesStorage.getFramePlan()), SimplePlan.<VisualFrameContext>of(context -> entities.processQueue(0))
.then(entitiesStorage.getFramePlan()), SimplePlan.<VisualFrameContext>of(context -> effects.processQueue(0))
.then(effectsStorage.getFramePlan())));
.to(NestedPlan.of(blockEntities.framePlan(), entities.framePlan(), effects.framePlan()));
framePlan = IfElsePlan.on((RenderContext ctx) -> engine.updateRenderOrigin(ctx.camera()))
.ifTrue(recreate)

View File

@ -3,9 +3,13 @@ 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.visual.VisualFrameContext;
import com.jozufozu.flywheel.api.visual.VisualTickContext;
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<>();
@ -60,4 +64,13 @@ public class VisualManagerImpl<T, S extends Storage<T>> implements VisualManager
}
}
public Plan<VisualFrameContext> framePlan() {
return SimplePlan.<VisualFrameContext>of(context -> processQueue(context.partialTick()))
.then(storage.getFramePlan());
}
public Plan<VisualTickContext> tickPlan() {
return SimplePlan.<VisualTickContext>of(context -> processQueue(0))
.then(storage.getTickPlan());
}
}

View File

@ -2,9 +2,13 @@ package com.jozufozu.flywheel.impl.visualization.storage;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicLong;
import org.jetbrains.annotations.NotNull;
import com.jozufozu.flywheel.api.task.Plan;
import com.jozufozu.flywheel.api.task.TaskExecutor;
import com.jozufozu.flywheel.api.visual.LitVisual;
@ -30,25 +34,28 @@ public class LitVisualStorage {
private final Map<LitVisual, LongSet> visuals2Sections = new WeakHashMap<>();
private final Long2ObjectMap<List<Updater>> sections2Visuals = new Long2ObjectOpenHashMap<>();
private final LongSet sectionsQueue = new LongOpenHashSet();
private final Queue<LitVisual> movedVisuals = new ConcurrentLinkedQueue<>();
private final LongSet sectionsUpdatedThisFrame = new LongOpenHashSet();
private long updateId = INITIAL_UPDATE_ID;
public Plan<VisualFrameContext> plan() {
return (SimplyComposedPlan<VisualFrameContext>) (TaskExecutor taskExecutor, VisualFrameContext context, Runnable onCompletion) -> {
if (sectionsQueue.isEmpty()) {
processMoved();
if (sectionsUpdatedThisFrame.isEmpty()) {
onCompletion.run();
return;
}
var sync = new Synchronizer(sectionsQueue.size(), () -> {
sectionsQueue.clear();
var sync = new Synchronizer(sectionsUpdatedThisFrame.size(), () -> {
sectionsUpdatedThisFrame.clear();
onCompletion.run();
});
long updateId = getNextUpdateId();
for (long section : sectionsQueue) {
for (long section : sectionsUpdatedThisFrame) {
var visuals = sections2Visuals.get(section);
if (visuals != null && !visuals.isEmpty()) {
taskExecutor.execute(() -> PlanUtil.distribute(taskExecutor, updateId, sync, visuals, Updater::updateLight));
@ -59,6 +66,16 @@ public class LitVisualStorage {
};
}
private void processMoved() {
LitVisual visual;
while ((visual = movedVisuals.poll()) != null) {
// If the visual isn't there when we try to remove it that means it was deleted before we got to it.
if (remove(visual)) {
add(visual);
}
}
}
private long getNextUpdateId() {
long out = this.updateId;
@ -75,37 +92,47 @@ public class LitVisualStorage {
return visuals2Sections.isEmpty();
}
public void add(LitVisual visual) {
public void addAndInitNotifier(LitVisual visual) {
visual.initLightSectionNotifier(new LitVisualNotifierImpl(visual));
add(visual);
}
private void add(LitVisual visual) {
LongSet sections = new LongArraySet();
visual.collectLightSections(sections::add);
Updater updater;
// Add the visual to the map even if sections is empty, this way we can distinguish from deleted visuals
visuals2Sections.put(visual, sections);
// Don't bother creating an updater if the visual isn't in any sections.
if (sections.isEmpty()) {
return;
} else if (sections.size() == 1) {
updater = new Updater.Simple(visual);
} else {
updater = new Updater.Synced(visual, new AtomicLong(NEVER_UPDATED));
}
var updater = createUpdater(visual, sections.size());
for (long section : sections) {
sections2Visuals.computeIfAbsent(section, $ -> new ObjectArrayList<>())
.add(updater);
}
visuals2Sections.put(visual, sections);
}
public void enqueueLightUpdateSections(LongSet sections) {
sectionsQueue.addAll(sections);
sectionsUpdatedThisFrame.addAll(sections);
}
public void remove(LitVisual visual) {
/**
* Remove the visual from this storage.
*
* @param visual The visual to remove.
* @return {@code true} if the visual was removed, {@code false} otherwise.
*/
public boolean remove(LitVisual visual) {
var sections = visuals2Sections.remove(visual);
if (sections == null) {
return;
return false;
}
for (long section : sections) {
@ -114,12 +141,14 @@ public class LitVisualStorage {
listeners.remove(indexOfUpdater(listeners, visual));
}
}
return true;
}
public void clear() {
visuals2Sections.clear();
sections2Visuals.clear();
sectionsQueue.clear();
sectionsUpdatedThisFrame.clear();
}
private static int indexOfUpdater(List<Updater> listeners, LitVisual visual) {
@ -132,6 +161,15 @@ public class LitVisualStorage {
return -1;
}
@NotNull
private static Updater createUpdater(LitVisual visual, int sectionCount) {
if (sectionCount == 1) {
return new Updater.Simple(visual);
} else {
return new Updater.Synced(visual, new AtomicLong(NEVER_UPDATED));
}
}
// Breaking this into 2 separate cases allows us to avoid the sync overhead in the common case.
// TODO: is it faster to only use the synced variant to avoid virtual dispatches?
sealed interface Updater {
@ -160,4 +198,17 @@ public class LitVisualStorage {
}
}
}
private final class LitVisualNotifierImpl implements LitVisual.Notifier {
private final LitVisual litVisual;
private LitVisualNotifierImpl(LitVisual litVisual) {
this.litVisual = litVisual;
}
@Override
public void notifySectionsChanged() {
movedVisuals.add(litVisual);
}
}
}

View File

@ -169,7 +169,7 @@ public abstract class Storage<T> {
}
if (visual instanceof LitVisual lit) {
litVisuals.add(lit);
litVisuals.addAndInitNotifier(lit);
}
}

View File

@ -2,6 +2,7 @@ package com.jozufozu.flywheel.lib.visual;
import java.util.function.LongConsumer;
import org.jetbrains.annotations.Nullable;
import org.joml.FrustumIntersection;
import com.jozufozu.flywheel.api.visual.BlockEntityVisual;
@ -40,6 +41,8 @@ public abstract class AbstractBlockEntityVisual<T extends BlockEntity> extends A
protected final BlockPos pos;
protected final BlockPos visualPos;
protected final BlockState blockState;
@Nullable
protected LitVisual.Notifier notifier;
public AbstractBlockEntityVisual(VisualizationContext ctx, T blockEntity) {
super(ctx, blockEntity.getLevel());
@ -59,6 +62,11 @@ public abstract class AbstractBlockEntityVisual<T extends BlockEntity> extends A
consumer.accept(SectionPos.asLong(pos));
}
@Override
public void initLightSectionNotifier(Notifier notifier) {
this.notifier = notifier;
}
@Override
public boolean shouldReset() {
return blockEntity.getBlockState() != blockState;