diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionLighter.java b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionLighter.java index ee1f26352..3ab1805e5 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionLighter.java +++ b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/ContraptionLighter.java @@ -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 { +public abstract class ContraptionLighter implements LightUpdateListener { protected final C contraption; public final LightVolume lightVolume; @@ -21,14 +26,8 @@ public abstract class ContraptionLighter { 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 { } 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; + } } diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/NonStationaryLighter.java b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/NonStationaryLighter.java index f93fd9beb..5678f103c 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/NonStationaryLighter.java +++ b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/NonStationaryLighter.java @@ -25,6 +25,8 @@ public class NonStationaryLighter extends ContraptionLigh if (!contraptionBounds.sameAs(bounds)) { lightVolume.move(contraption.entity.world, contraptionBoundsToVolume(contraptionBounds)); bounds = contraptionBounds; + + startListening(); } } diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/render/ActorInstance.java b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/render/ActorInstance.java index 3991672c7..9507a05ef 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/render/ActorInstance.java +++ b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/render/ActorInstance.java @@ -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); } } diff --git a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/render/ContraptionKineticRenderer.java b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/render/ContraptionKineticRenderer.java index 612f57787..c6b3df6e6 100644 --- a/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/render/ContraptionKineticRenderer.java +++ b/src/main/java/com/simibubi/create/content/contraptions/components/structureMovement/render/ContraptionKineticRenderer.java @@ -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 { protected ArrayList actors = new ArrayList<>(); - public final RenderedContraption contraption; + private final WeakReference contraption; ContraptionKineticRenderer(RenderedContraption contraption) { - this.contraption = contraption; + this.contraption = new WeakReference<>(contraption); } @Override @@ -77,6 +78,10 @@ public class ContraptionKineticRenderer extends InstancedTileRenderer> 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); diff --git a/src/main/java/com/simibubi/create/foundation/mixin/LightUpdateMixin.java b/src/main/java/com/simibubi/create/foundation/mixin/LightUpdateMixin.java index d55539c7c..81501b16c 100644 --- a/src/main/java/com/simibubi/create/foundation/mixin/LightUpdateMixin.java +++ b/src/main/java/com/simibubi/create/foundation/mixin/LightUpdateMixin.java @@ -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()); } } diff --git a/src/main/java/com/simibubi/create/foundation/mixin/NetworkLightUpdateMixin.java b/src/main/java/com/simibubi/create/foundation/mixin/NetworkLightUpdateMixin.java index a40c2c088..9e775c094 100644 --- a/src/main/java/com/simibubi/create/foundation/mixin/NetworkLightUpdateMixin.java +++ b/src/main/java/com/simibubi/create/foundation/mixin/NetworkLightUpdateMixin.java @@ -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); }); } } diff --git a/src/main/java/com/simibubi/create/foundation/render/backend/light/LightUpdateListener.java b/src/main/java/com/simibubi/create/foundation/render/backend/light/LightUpdateListener.java new file mode 100644 index 000000000..48de4e397 --- /dev/null +++ b/src/main/java/com/simibubi/create/foundation/render/backend/light/LightUpdateListener.java @@ -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); + } +} diff --git a/src/main/java/com/simibubi/create/foundation/render/backend/light/LightUpdater.java b/src/main/java/com/simibubi/create/foundation/render/backend/light/LightUpdater.java new file mode 100644 index 000000000..dc6a7ca7b --- /dev/null +++ b/src/main/java/com/simibubi/create/foundation/render/backend/light/LightUpdater.java @@ -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> sections; + private final WeakHashMap listenersToSections; + + private final Long2ObjectMap> chunks; + private final WeakHashMap 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 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 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 listeners, Long2ObjectMap> lookup) { + LongRBTreeSet set = listeners.get(listener); + + if (set == null) { + set = new LongRBTreeSet(); + listeners.put(listener, set); + } else { + set.forEach((LongConsumer) l -> { + WeakHashSet 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 getOrCreate(Long2ObjectMap> sections, long chunkPos) { + WeakHashSet 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; + } +} diff --git a/src/main/java/com/simibubi/create/foundation/render/backend/light/LightVolume.java b/src/main/java/com/simibubi/create/foundation/render/backend/light/LightVolume.java index 5f9290974..7c80e0765 100644 --- a/src/main/java/com/simibubi/create/foundation/render/backend/light/LightVolume.java +++ b/src/main/java/com/simibubi/create/foundation/render/backend/light/LightVolume.java @@ -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; diff --git a/src/main/java/com/simibubi/create/foundation/utility/WeakHashSet.java b/src/main/java/com/simibubi/create/foundation/utility/WeakHashSet.java new file mode 100644 index 000000000..e4c5c6688 --- /dev/null +++ b/src/main/java/com/simibubi/create/foundation/utility/WeakHashSet.java @@ -0,0 +1,114 @@ +package com.simibubi.create.foundation.utility; + +import net.minecraft.util.Unit; + +import java.util.*; + +public class WeakHashSet extends AbstractSet { + + WeakHashMap map; + + public WeakHashSet() { + map = new WeakHashMap<>(); + } + + /** + * Constructs a new set containing the elements in the specified + * collection. The HashMap 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 c) { + map = new WeakHashMap<>(Math.max((int) (c.size()/.75f) + 1, 16)); + addAll(c); + } + + /** + * Constructs a new, empty set; the backing HashMap 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 HashMap 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 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 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(); + } +}