Spicy light update listening api.

- Round 1, no profiling done yet, not everything uses it.
 - WeakHashSet could be useful elsewhere, too.
This commit is contained in:
JozsefA 2021-03-23 00:08:31 -07:00
parent 1310b88828
commit 20189a86fc
11 changed files with 400 additions and 27 deletions

View file

@ -1,10 +1,15 @@
package com.simibubi.create.content.contraptions.components.structureMovement;
import net.minecraft.world.ILightReader;
import net.minecraft.world.LightType;
import com.simibubi.create.content.contraptions.components.structureMovement.render.RenderedContraption;
import com.simibubi.create.foundation.render.backend.light.GridAlignedBB;
import com.simibubi.create.foundation.render.backend.light.LightUpdateListener;
import com.simibubi.create.foundation.render.backend.light.LightUpdater;
import com.simibubi.create.foundation.render.backend.light.LightVolume;
public abstract class ContraptionLighter<C extends Contraption> {
public abstract class ContraptionLighter<C extends Contraption> implements LightUpdateListener {
protected final C contraption;
public final LightVolume lightVolume;
@ -21,14 +26,8 @@ public abstract class ContraptionLighter<C extends Contraption> {
lightVolume.initialize(contraption.entity.world);
scheduleRebuild = true;
}
protected GridAlignedBB contraptionBoundsToVolume(GridAlignedBB bounds) {
bounds.grow(1); // so we have at least enough data on the edges to avoid artifacts and have smooth lighting
bounds.minY = Math.max(bounds.minY, 0);
bounds.maxY = Math.min(bounds.maxY, 255);
return bounds;
startListening();
}
public void tick(RenderedContraption owner) {
@ -39,4 +38,26 @@ public abstract class ContraptionLighter<C extends Contraption> {
}
public abstract GridAlignedBB getContraptionBounds();
@Override
public void onLightUpdate(ILightReader world, LightType type, GridAlignedBB changed) {
lightVolume.notifyLightUpdate(world, type, changed);
}
@Override
public void onLightPacket(ILightReader world, int chunkX, int chunkZ) {
lightVolume.notifyLightPacket(world, chunkX, chunkZ);
}
protected void startListening() {
LightUpdater.getInstance().startListening(bounds, this);
}
protected GridAlignedBB contraptionBoundsToVolume(GridAlignedBB bounds) {
bounds.grow(1); // so we have at least enough data on the edges to avoid artifacts and have smooth lighting
bounds.minY = Math.max(bounds.minY, 0);
bounds.maxY = Math.min(bounds.maxY, 255);
return bounds;
}
}

View file

@ -25,6 +25,8 @@ public class NonStationaryLighter<C extends Contraption> extends ContraptionLigh
if (!contraptionBounds.sameAs(bounds)) {
lightVolume.move(contraption.entity.world, contraptionBoundsToVolume(contraptionBounds));
bounds = contraptionBounds;
startListening();
}
}

View file

@ -17,6 +17,6 @@ public abstract class ActorInstance {
public void beginFrame() { }
protected int localBlockLight() {
return modelManager.contraption.renderWorld.getLightLevel(LightType.BLOCK, context.localPos);
return modelManager.getContraption().renderWorld.getLightLevel(LightType.BLOCK, context.localPos);
}
}

View file

@ -20,16 +20,17 @@ import net.minecraft.world.gen.feature.template.Template;
import org.apache.commons.lang3.tuple.Pair;
import javax.annotation.Nullable;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
public class ContraptionKineticRenderer extends InstancedTileRenderer<ContraptionProgram> {
protected ArrayList<com.simibubi.create.content.contraptions.components.structureMovement.render.ActorInstance> actors = new ArrayList<>();
public final RenderedContraption contraption;
private final WeakReference<RenderedContraption> contraption;
ContraptionKineticRenderer(RenderedContraption contraption) {
this.contraption = contraption;
this.contraption = new WeakReference<>(contraption);
}
@Override
@ -77,6 +78,10 @@ public class ContraptionKineticRenderer extends InstancedTileRenderer<Contraptio
return getMaterial(KineticRenderMaterials.ACTORS);
}
public RenderedContraption getContraption() {
return contraption.get();
}
@Override
public BlockPos getOriginCoordinate() {
return BlockPos.ZERO;

View file

@ -54,12 +54,6 @@ public class ContraptionRenderDispatcher {
public static final Compartment<Pair<Contraption, Integer>> CONTRAPTION = new Compartment<>();
protected static PlacementSimulationWorld renderWorld;
public static void notifyLightUpdate(ILightReader world, LightType type, SectionPos pos) {
for (RenderedContraption renderer : renderers.values()) {
renderer.getLighter().lightVolume.notifyLightUpdate(world, type, pos);
}
}
public static void notifyLightPacket(ILightReader world, int chunkX, int chunkZ) {
for (RenderedContraption renderer : renderers.values()) {
renderer.getLighter().lightVolume.notifyLightPacket(world, chunkX, chunkZ);

View file

@ -11,6 +11,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import com.simibubi.create.content.contraptions.components.structureMovement.render.ContraptionRenderDispatcher;
import com.simibubi.create.foundation.render.backend.light.ILightListener;
import com.simibubi.create.foundation.render.backend.light.LightUpdater;
import net.minecraft.client.multiplayer.ClientChunkProvider;
import net.minecraft.util.math.SectionPos;
@ -54,6 +55,6 @@ public abstract class LightUpdateMixin extends AbstractChunkProvider {
});
}
ContraptionRenderDispatcher.notifyLightUpdate(world, type, pos);
LightUpdater.getInstance().onLightUpdate(world, type, pos.asLong());
}
}

View file

@ -1,13 +1,19 @@
package com.simibubi.create.foundation.mixin;
import com.simibubi.create.CreateClient;
import com.simibubi.create.content.contraptions.components.structureMovement.render.ContraptionRenderDispatcher;
import com.simibubi.create.foundation.render.backend.RenderWork;
import com.simibubi.create.foundation.render.backend.light.ILightListener;
import com.simibubi.create.foundation.render.backend.light.LightUpdater;
import net.minecraft.client.Minecraft;
import net.minecraft.client.network.play.ClientPlayNetHandler;
import net.minecraft.client.world.ClientWorld;
import net.minecraft.network.play.server.SUpdateLightPacket;
import net.minecraft.util.math.SectionPos;
import net.minecraft.world.chunk.Chunk;
import java.util.Map;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
@ -30,14 +36,16 @@ public class NetworkLightUpdateMixin {
if (chunk != null) {
chunk.getTileEntityMap()
.values()
.stream()
.filter(tile -> tile instanceof ILightListener)
.map(tile -> (ILightListener) tile)
.forEach(ILightListener::onChunkLightUpdate);
.values()
.forEach(tile -> {
CreateClient.kineticRenderer.get(world).onLightUpdate(tile);
if (tile instanceof ILightListener)
((ILightListener) tile).onChunkLightUpdate();
});
}
ContraptionRenderDispatcher.notifyLightPacket(world, chunkX, chunkZ);
LightUpdater.getInstance().onLightPacket(world, chunkX, chunkZ);
});
}
}

View file

@ -0,0 +1,26 @@
package com.simibubi.create.foundation.render.backend.light;
import net.minecraft.world.ILightReader;
import net.minecraft.world.LightType;
/**
* Anything can implement this, implementors should call {@link LightUpdater#startListening}
* appropriately to make sure they get the updates they want.
*/
public interface LightUpdateListener {
/**
* Called when a light updates in a chunk the implementor cares about.
*/
void onLightUpdate(ILightReader world, LightType type, GridAlignedBB changed);
/**
* Called when the server sends light data to the client.
*/
default void onLightPacket(ILightReader world, int chunkX, int chunkZ) {
GridAlignedBB changedVolume = GridAlignedBB.fromChunk(chunkX, chunkZ);
onLightUpdate(world, LightType.BLOCK, changedVolume);
onLightUpdate(world, LightType.SKY, changedVolume);
}
}

View file

@ -0,0 +1,198 @@
package com.simibubi.create.foundation.render.backend.light;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.LongRBTreeSet;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.SectionPos;
import net.minecraft.world.ILightReader;
import net.minecraft.world.LightType;
import java.util.*;
import java.util.function.LongConsumer;
import com.simibubi.create.foundation.utility.WeakHashSet;
/**
* By using WeakReferences we can automatically remove listeners when they are garbage collected.
* This allows us to easily be more clever about how we store the listeners. Each listener is associated
* with 2 sets of longs indicating what chunks and sections each listener is in. Additionally, a reverse
* mapping is created to allow for fast lookups when light updates. The reverse mapping is more interesting,
* but {@link #listenersToSections}, and {@link #listenersToChunks} are used to know what sections and
* chunks we need to remove the listeners from if they re-subscribe. Otherwise, listeners could get updates
* they no longer care about. This is done in {@link #clearSections} and {@link #clearChunks}
*/
public class LightUpdater {
private static LightUpdater instance;
public static LightUpdater getInstance() {
if (instance == null)
instance = new LightUpdater();
return instance;
}
private final Long2ObjectMap<WeakHashSet<LightUpdateListener>> sections;
private final WeakHashMap<LightUpdateListener, LongRBTreeSet> listenersToSections;
private final Long2ObjectMap<WeakHashSet<LightUpdateListener>> chunks;
private final WeakHashMap<LightUpdateListener, LongRBTreeSet> listenersToChunks;
public LightUpdater() {
sections = new Long2ObjectOpenHashMap<>();
listenersToSections = new WeakHashMap<>();
chunks = new Long2ObjectOpenHashMap<>();
listenersToChunks = new WeakHashMap<>();
}
/**
* Add a listener associated with the given {@link BlockPos}.
*
* When a light update occurs in the chunk the position is contained in,
* {@link LightUpdateListener#onLightUpdate} will be called.
*
* @param pos The position in the world that the listener cares about.
* @param listener The object that wants to receive light update notifications.
*/
public void startListening(BlockPos pos, LightUpdateListener listener) {
LongRBTreeSet sections = clearSections(listener);
LongRBTreeSet chunks = clearChunks(listener);
long sectionPos = worldToSection(pos);
addToSection(sectionPos, listener);
sections.add(sectionPos);
long chunkPos = sectionToChunk(sectionPos);
addToChunk(chunkPos, listener);
chunks.add(chunkPos);
}
/**
* Add a listener associated with the given {@link GridAlignedBB}.
*
* When a light update occurs in any chunk spanning the given volume,
* {@link LightUpdateListener#onLightUpdate} will be called.
*
* @param volume The volume in the world that the listener cares about.
* @param listener The object that wants to receive light update notifications.
*/
public void startListening(GridAlignedBB volume, LightUpdateListener listener) {
LongRBTreeSet sections = clearSections(listener);
LongRBTreeSet chunks = clearSections(listener);
int minX = SectionPos.toChunk(volume.minX);
int minY = SectionPos.toChunk(volume.minY);
int minZ = SectionPos.toChunk(volume.minZ);
int maxX = SectionPos.toChunk(volume.maxX);
int maxY = SectionPos.toChunk(volume.maxY);
int maxZ = SectionPos.toChunk(volume.maxZ);
for (int x = minX; x <= maxX; x++) {
for (int z = minZ; z <= maxZ; z++) {
for (int y = minY; y <= maxY; y++) {
long sectionPos = SectionPos.asLong(x, y, z);
addToSection(sectionPos, listener);
sections.add(sectionPos);
}
long chunkPos = SectionPos.asLong(x, 0, z);
addToChunk(chunkPos, listener);
chunks.add(chunkPos);
}
}
}
/**
* Dispatch light updates to all registered {@link LightUpdateListener}s.
*
* @param world The world in which light was updated.
* @param type The type of light that changed.
* @param sectionPos A long representing the section position where light changed.
*/
public void onLightUpdate(ILightReader world, LightType type, long sectionPos) {
WeakHashSet<LightUpdateListener> set = sections.get(sectionPos);
if (set == null || set.isEmpty()) return;
GridAlignedBB chunkBox = GridAlignedBB.fromSection(SectionPos.from(sectionPos));
for (LightUpdateListener listener : set) {
listener.onLightUpdate(world, type, chunkBox.copy());
}
}
/**
* Dispatch light updates to all registered {@link LightUpdateListener}s
* when the server sends lighting data for an entire chunk.
*
* @param world The world in which light was updated.
*/
public void onLightPacket(ILightReader world, int chunkX, int chunkZ) {
long chunkPos = SectionPos.asLong(chunkX, 0, chunkZ);
WeakHashSet<LightUpdateListener> set = chunks.get(chunkPos);
if (set == null || set.isEmpty()) return;
for (LightUpdateListener listener : set) {
listener.onLightPacket(world, chunkX, chunkZ);
}
}
private LongRBTreeSet clearChunks(LightUpdateListener listener) {
return clear(listener, listenersToChunks, chunks);
}
private LongRBTreeSet clearSections(LightUpdateListener listener) {
return clear(listener, listenersToSections, sections);
}
private LongRBTreeSet clear(LightUpdateListener listener, WeakHashMap<LightUpdateListener, LongRBTreeSet> listeners, Long2ObjectMap<WeakHashSet<LightUpdateListener>> lookup) {
LongRBTreeSet set = listeners.get(listener);
if (set == null) {
set = new LongRBTreeSet();
listeners.put(listener, set);
} else {
set.forEach((LongConsumer) l -> {
WeakHashSet<LightUpdateListener> listeningSections = lookup.get(l);
if (listeningSections != null) listeningSections.remove(listener);
});
set.clear();
}
return set;
}
private void addToSection(long sectionPos, LightUpdateListener listener) {
getOrCreate(sections, sectionPos).add(listener);
}
private void addToChunk(long chunkPos, LightUpdateListener listener) {
getOrCreate(chunks, chunkPos).add(listener);
}
private WeakHashSet<LightUpdateListener> getOrCreate(Long2ObjectMap<WeakHashSet<LightUpdateListener>> sections, long chunkPos) {
WeakHashSet<LightUpdateListener> set = sections.get(chunkPos);
if (set == null) {
set = new WeakHashSet<>();
sections.put(chunkPos, set);
}
return set;
}
public static long worldToSection(BlockPos pos) {
return SectionPos.asLong(pos.getX(), pos.getY(), pos.getZ());
}
public static long sectionToChunk(long sectionPos) {
return sectionPos & 0xFFFFFFFFFFF_00000L;
}
}

View file

@ -123,17 +123,21 @@ public class LightVolume {
}
}
public void notifyLightUpdate(ILightReader world, LightType type, SectionPos location) {
GridAlignedBB changedVolume = GridAlignedBB.fromSection(location);
public void notifyLightUpdate(ILightReader world, LightType type, GridAlignedBB changedVolume) {
if (removed)
return;
if (!changedVolume.intersects(sampleVolume))
return;
changedVolume.intersectAssign(sampleVolume); // compute the region contained by us that has dirty lighting data.
changedVolume = changedVolume.intersect(sampleVolume); // compute the region contained by us that has dirty lighting data.
if (type == LightType.BLOCK) copyBlock(world, changedVolume);
else if (type == LightType.SKY) copySky(world, changedVolume);
}
public void notifyLightPacket(ILightReader world, int chunkX, int chunkZ) {
if (removed) return;
GridAlignedBB changedVolume = GridAlignedBB.fromChunk(chunkX, chunkZ);
if (!changedVolume.intersects(sampleVolume))
return;

View file

@ -0,0 +1,114 @@
package com.simibubi.create.foundation.utility;
import net.minecraft.util.Unit;
import java.util.*;
public class WeakHashSet<T> extends AbstractSet<T> {
WeakHashMap<T, Unit> map;
public WeakHashSet() {
map = new WeakHashMap<>();
}
/**
* Constructs a new set containing the elements in the specified
* collection. The <tt>HashMap</tt> is created with default load factor
* (0.75) and an initial capacity sufficient to contain the elements in
* the specified collection.
*
* @param c the collection whose elements are to be placed into this set
* @throws NullPointerException if the specified collection is null
*/
public WeakHashSet(Collection<? extends T> c) {
map = new WeakHashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* the specified initial capacity and the specified load factor.
*
* @param initialCapacity the initial capacity of the hash map
* @param loadFactor the load factor of the hash map
* @throws IllegalArgumentException if the initial capacity is less
* than zero, or if the load factor is nonpositive
*/
public WeakHashSet(int initialCapacity, float loadFactor) {
map = new WeakHashMap<>(initialCapacity, loadFactor);
}
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* the specified initial capacity and default load factor (0.75).
*
* @param initialCapacity the initial capacity of the hash table
* @throws IllegalArgumentException if the initial capacity is less
* than zero
*/
public WeakHashSet(int initialCapacity) {
map = new WeakHashMap<>(initialCapacity);
}
@Override
public Iterator<T> iterator() {
return map.keySet().iterator();
}
@Override
public int size() {
return map.size();
}
@Override
public boolean add(T t) {
return map.put(t, Unit.INSTANCE) == null;
}
@Override
public boolean remove(Object o) {
return map.remove((T) o) != null;
}
@Override
public boolean isEmpty() {
return map.isEmpty();
}
@Override
public boolean contains(Object o) {
return map.containsKey((T) o);
}
@Override
public Object[] toArray() {
return map.keySet().toArray();
}
@Override
public boolean containsAll(Collection<?> c) {
return c.stream().allMatch(map::containsKey);
}
@Override
public boolean addAll(Collection<? extends T> c) {
return false;
}
@Override
public boolean retainAll(Collection<?> c) {
return false;
}
@Override
public boolean removeAll(Collection<?> c) {
return false;
}
@Override
public void clear() {
map.clear();
}
}