From 12aa1e6b7bd7f3954c1ad6ace79bf865d37e6ee0 Mon Sep 17 00:00:00 2001 From: iam Date: Fri, 4 Aug 2023 16:08:24 -0400 Subject: [PATCH] hollow-cube/lighting-memory-reduction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lighting reduce memory + Fix lighting not sending + Performance (#31) * Reduce memory * Clone * Executor pool + cleanup * Cleanup * Don't batch, it's slower * Parallel chunk loading for test * Check below chunk 6. Sky light data doesn't appear to be saved above the highest point in the chunk height map. * Fix weird locking * ඞ * Fix test * Fix indentation * Use short instead of int * Use short instead of int * Start removing borders * Borders gone * Cleanup * Cleanup * Remove borders fully - Still needs cleanup * Cleanup 1 * Cleanup 2 * Cleanup 3 * Cleanup 4 * Cache * Performance * Performance * Cleanup * Cleanup * Refactor * Cleanup from self-review --- .../server/instance/LightingChunk.java | 129 ++++++------- .../net/minestom/server/instance/Section.java | 15 +- .../server/instance/light/BlockLight.java | 162 +++++----------- .../minestom/server/instance/light/Light.java | 44 ++++- .../server/instance/light/LightCompute.java | 39 ++-- .../server/instance/light/SkyLight.java | 181 ++++++------------ .../light/LightParityIntegrationTest.java | 24 ++- 7 files changed, 254 insertions(+), 340 deletions(-) diff --git a/src/main/java/net/minestom/server/instance/LightingChunk.java b/src/main/java/net/minestom/server/instance/LightingChunk.java index 1528c265e..de355730b 100644 --- a/src/main/java/net/minestom/server/instance/LightingChunk.java +++ b/src/main/java/net/minestom/server/instance/LightingChunk.java @@ -21,10 +21,11 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; import static net.minestom.server.instance.light.LightCompute.emptyContent; @@ -33,15 +34,23 @@ public class LightingChunk extends DynamicChunk { private static final int LIGHTING_CHUNKS_PER_SEND = Integer.getInteger("minestom.lighting.chunks-per-send", 10); private static final int LIGHTING_CHUNKS_SEND_DELAY = Integer.getInteger("minestom.lighting.chunks-send-delay", 100); + private static final ExecutorService pool = Executors.newWorkStealingPool(); + private int[] heightmap; final CachedPacket lightCache = new CachedPacket(this::createLightPacket); boolean sendNeighbours = true; + boolean chunkLoaded = false; enum LightType { SKY, BLOCK } + private enum QueueType { + INTERNAL, + EXTERNAL + } + private static final Set DIFFUSE_SKY_LIGHT = Set.of( Block.COBWEB.namespace(), Block.ICE.namespace(), @@ -111,9 +120,10 @@ public class LightingChunk extends DynamicChunk { // Invalidate neighbor chunks, since they can be updated by this block change int coordinate = ChunkUtils.getChunkCoordinate(y); - invalidateSection(coordinate); - - this.lightCache.invalidate(); + if (chunkLoaded) { + invalidateSection(coordinate); + this.lightCache.invalidate(); + } } public void sendLighting() { @@ -124,6 +134,7 @@ public class LightingChunk extends DynamicChunk { @Override protected void onLoad() { // Prefetch the chunk packet so that lazy lighting is computed + chunkLoaded = true; updateAfterGeneration(this); } @@ -276,7 +287,7 @@ public class LightingChunk extends DynamicChunk { for (LightingChunk f : copy) { if (f.isLoaded()) { f.sendLighting(); - if (f.getViewers().size() == 0) return; + if (f.getViewers().size() == 0) continue; } count++; @@ -293,51 +304,56 @@ public class LightingChunk extends DynamicChunk { lightLock.unlock(); } - private static void flushQueue(Instance instance, Set queue, LightType type) { - var updateQueue = - queue.parallelStream() - .map(sectionLocation -> { - Chunk chunk = instance.getChunk(sectionLocation.blockX(), sectionLocation.blockZ()); - if (chunk == null) return null; + private static void flushQueue(Instance instance, Set queue, LightType type, QueueType queueType) { + AtomicInteger count = new AtomicInteger(0); + Set sections = ConcurrentHashMap.newKeySet(); + Set newQueue = ConcurrentHashMap.newKeySet(); - if (type == LightType.BLOCK) { - return chunk.getSection(sectionLocation.blockY()).blockLight() - .calculateExternal(instance, chunk, sectionLocation.blockY()); - } else { - return chunk.getSection(sectionLocation.blockY()).skyLight() - .calculateExternal(instance, chunk, sectionLocation.blockY()); - } - }) - .filter(Objects::nonNull) - .toList() - .parallelStream() - .flatMap(light -> light.flip().stream()) - .collect(Collectors.toSet()); + for (Point point : queue) { + Chunk chunk = instance.getChunk(point.blockX(), point.blockZ()); + if (chunk == null) { + count.getAndIncrement(); + continue; + } - if (updateQueue.size() > 0) { - flushQueue(instance, updateQueue, type); + var light = type == LightType.BLOCK ? chunk.getSection(point.blockY()).blockLight() : chunk.getSection(point.blockY()).skyLight(); + + pool.submit(() -> { + if (queueType == QueueType.INTERNAL) light.calculateInternal(instance, chunk.getChunkX(), point.blockY(), chunk.getChunkZ()); + else light.calculateExternal(instance, chunk, point.blockY()); + + sections.add(light); + + var toAdd = light.flip(); + if (toAdd != null) newQueue.addAll(toAdd); + + count.incrementAndGet(); + }); + } + + while (count.get() < queue.size()) { } + + if (newQueue.size() > 0) { + flushQueue(instance, newQueue, type, QueueType.EXTERNAL); } } public static void relight(Instance instance, Collection chunks) { - Set toPropagate = chunks - .parallelStream() - .flatMap(chunk -> IntStream - .range(chunk.getMinSection(), chunk.getMaxSection()) - .mapToObj(index -> Map.entry(index, chunk))) - .map(chunkIndex -> { - final Chunk chunk = chunkIndex.getValue(); - final int section = chunkIndex.getKey(); + Set sections = new HashSet<>(); - chunk.getSection(section).blockLight().invalidate(); - chunk.getSection(section).skyLight().invalidate(); + for (Chunk chunk : chunks) { + if (chunk == null) continue; + for (int section = chunk.minSection; section < chunk.maxSection; section++) { + chunk.getSection(section).blockLight().invalidate(); + chunk.getSection(section).skyLight().invalidate(); - return new Vec(chunk.getChunkX(), section, chunk.getChunkZ()); - }).collect(Collectors.toSet()); + sections.add(new Vec(chunk.getChunkX(), section, chunk.getChunkZ())); + } + } synchronized (instance) { - relight(instance, toPropagate, LightType.BLOCK); - relight(instance, toPropagate, LightType.SKY); + relight(instance, sections, LightType.BLOCK); + relight(instance, sections, LightType.SKY); } } @@ -404,33 +420,8 @@ public class LightingChunk extends DynamicChunk { } } - private static void relight(Instance instance, Set sections, LightType type) { - Set toPropagate = sections - .parallelStream() - // .stream() - .map(chunkIndex -> { - final Chunk chunk = instance.getChunk(chunkIndex.blockX(), chunkIndex.blockZ()); - final int section = chunkIndex.blockY(); - if (chunk == null) return null; - if (type == LightType.BLOCK) return chunk.getSection(section).blockLight().calculateInternal(chunk.getInstance(), chunk.getChunkX(), section, chunk.getChunkZ()); - else return chunk.getSection(section).skyLight().calculateInternal(chunk.getInstance(), chunk.getChunkX(), section, chunk.getChunkZ()); - }).filter(Objects::nonNull) - .flatMap(lightSet -> lightSet.flip().stream()) - .collect(Collectors.toSet()) - // .stream() - .parallelStream() - .flatMap(sectionLocation -> { - final Chunk chunk = instance.getChunk(sectionLocation.blockX(), sectionLocation.blockZ()); - final int section = sectionLocation.blockY(); - if (chunk == null) return Stream.empty(); - - final Light light = type == LightType.BLOCK ? chunk.getSection(section).blockLight() : chunk.getSection(section).skyLight(); - light.calculateExternal(chunk.getInstance(), chunk, section); - - return light.flip().stream(); - }).collect(Collectors.toSet()); - - flushQueue(instance, toPropagate, type); + private static void relight(Instance instance, Set queue, LightType type) { + flushQueue(instance, queue, type, QueueType.INTERNAL); } @Override diff --git a/src/main/java/net/minestom/server/instance/Section.java b/src/main/java/net/minestom/server/instance/Section.java index 789e56c6e..1222856a7 100644 --- a/src/main/java/net/minestom/server/instance/Section.java +++ b/src/main/java/net/minestom/server/instance/Section.java @@ -20,6 +20,13 @@ public final class Section implements NetworkBuffer.Writer { this.blockLight = Light.block(blockPalette); } + private Section(Palette blockPalette, Palette biomePalette, Light skyLight, Light blockLight) { + this.blockPalette = blockPalette; + this.biomePalette = biomePalette; + this.skyLight = skyLight; + this.blockLight = blockLight; + } + public Section() { this(Palette.blocks(), Palette.biomes()); } @@ -39,7 +46,13 @@ public final class Section implements NetworkBuffer.Writer { @Override public @NotNull Section clone() { - return new Section(this.blockPalette.clone(), this.biomePalette.clone()); + final Light skyLight = Light.sky(blockPalette); + final Light blockLight = Light.block(blockPalette); + + skyLight.set(this.skyLight.array()); + blockLight.set(this.blockLight.array()); + + return new Section(this.blockPalette.clone(), this.biomePalette.clone(), skyLight, blockLight); } @Override diff --git a/src/main/java/net/minestom/server/instance/light/BlockLight.java b/src/main/java/net/minestom/server/instance/light/BlockLight.java index 365321c50..779264616 100644 --- a/src/main/java/net/minestom/server/instance/light/BlockLight.java +++ b/src/main/java/net/minestom/server/instance/light/BlockLight.java @@ -1,6 +1,6 @@ package net.minestom.server.instance.light; -import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue; +import it.unimi.dsi.fastutil.shorts.ShortArrayFIFOQueue; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.Chunk; @@ -15,6 +15,7 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import static net.minestom.server.instance.light.LightCompute.*; @@ -25,12 +26,11 @@ final class BlockLight implements Light { private byte[] contentPropagation; private byte[] contentPropagationSwap; - private byte[][] borders; - private byte[][] bordersPropagation; - private byte[][] bordersPropagationSwap; private boolean isValidBorders = true; private boolean needsSend = true; + private Set toUpdateSet = new HashSet<>(); + private final Section[] neighborSections = new Section[BlockFace.values().length]; BlockLight(Palette blockPalette) { this.blockPalette = blockPalette; @@ -38,21 +38,17 @@ final class BlockLight implements Light { @Override public Set flip() { - if (this.bordersPropagationSwap != null) - this.bordersPropagation = this.bordersPropagationSwap; - if (this.contentPropagationSwap != null) this.contentPropagation = this.contentPropagationSwap; - this.bordersPropagationSwap = null; this.contentPropagationSwap = null; if (toUpdateSet == null) return Set.of(); return toUpdateSet; } - static IntArrayFIFOQueue buildInternalQueue(Palette blockPalette, Block[] blocks) { - IntArrayFIFOQueue lightSources = new IntArrayFIFOQueue(); + static ShortArrayFIFOQueue buildInternalQueue(Palette blockPalette) { + ShortArrayFIFOQueue lightSources = new ShortArrayFIFOQueue(); // Apply section light blockPalette.getAllPresent((x, y, z, stateId) -> { final Block block = Block.fromStateId((short) stateId); @@ -60,9 +56,8 @@ final class BlockLight implements Light { final byte lightEmission = (byte) block.registry().lightEmission(); final int index = x | (z << 4) | (y << 8); - blocks[index] = block; if (lightEmission > 0) { - lightSources.enqueue(index | (lightEmission << 12)); + lightSources.enqueue((short) (index | (lightEmission << 12))); } }); return lightSources; @@ -72,41 +67,55 @@ final class BlockLight implements Light { return Block.fromStateId((short)palette.get(x, y, z)); } - private static IntArrayFIFOQueue buildExternalQueue(Instance instance, Block[] blocks, Map neighbors, byte[][] borders) { - IntArrayFIFOQueue lightSources = new IntArrayFIFOQueue(); + private ShortArrayFIFOQueue buildExternalQueue(Instance instance, Palette blockPalette, Point[] neighbors, byte[] content) { + ShortArrayFIFOQueue lightSources = new ShortArrayFIFOQueue(); - for (BlockFace face : BlockFace.values()) { - Point neighborSection = neighbors.get(face); + for (int i = 0; i < neighbors.length; i++) { + var face = BlockFace.values()[i]; + Point neighborSection = neighbors[i]; if (neighborSection == null) continue; - Chunk chunk = instance.getChunk(neighborSection.blockX(), neighborSection.blockZ()); - if (chunk == null) continue; + Section otherSection = neighborSections[face.ordinal()]; - byte[] neighborFace = chunk.getSection(neighborSection.blockY()).blockLight().getBorderPropagation(face.getOppositeFace()); - if (neighborFace == null) continue; + if (otherSection == null) { + Chunk chunk = instance.getChunk(neighborSection.blockX(), neighborSection.blockZ()); + if (chunk == null) continue; + + otherSection = chunk.getSection(neighborSection.blockY()); + neighborSections[face.ordinal()] = otherSection; + } + + var otherLight = otherSection.blockLight(); for (int bx = 0; bx < 16; bx++) { for (int by = 0; by < 16; by++) { - final int borderIndex = bx * SECTION_SIZE + by; - byte lightEmission = neighborFace[borderIndex]; - - if (borders != null && borders[face.ordinal()] != null) { - final int internalEmission = borders[face.ordinal()][borderIndex]; - if (lightEmission <= internalEmission) continue; - } - final int k = switch (face) { case WEST, BOTTOM, NORTH -> 0; case EAST, TOP, SOUTH -> 15; }; + final byte lightEmission = (byte) Math.max(switch (face) { + case NORTH, SOUTH -> (byte) otherLight.getLevel(bx, by, 15 - k); + case WEST, EAST -> (byte) otherLight.getLevel(15 - k, bx, by); + default -> (byte) otherLight.getLevel(bx, 15 - k, by); + } - 1, 0); + final int posTo = switch (face) { case NORTH, SOUTH -> bx | (k << 4) | (by << 8); case WEST, EAST -> k | (by << 4) | (bx << 8); default -> bx | (by << 4) | (k << 8); }; - Section otherSection = chunk.getSection(neighborSection.blockY()); + if (content != null) { + final int internalEmission = (byte) (Math.max(getLight(content, posTo) - 1, 0)); + if (lightEmission <= internalEmission) continue; + } + + final Block blockTo = switch(face) { + case NORTH, SOUTH -> getBlock(blockPalette, bx, by, k); + case WEST, EAST -> getBlock(blockPalette, k, bx, by); + default -> getBlock(blockPalette, bx, k, by); + }; final Block blockFrom = (switch (face) { case NORTH, SOUTH -> getBlock(otherSection.blockPalette(), bx, by, 15 - k); @@ -114,9 +123,6 @@ final class BlockLight implements Light { default -> getBlock(otherSection.blockPalette(), bx, 15 - k, by); }); - if (blocks == null) continue; - Block blockTo = blocks[posTo]; - if (blockTo == null && blockFrom != null) { if (blockFrom.registry().collisionShape().isOccluded(Block.AIR.registry().collisionShape(), face.getOppositeFace())) continue; @@ -130,7 +136,7 @@ final class BlockLight implements Light { if (lightEmission > 0) { final int index = posTo | (lightEmission << 12); - lightSources.enqueue(index); + lightSources.enqueue((short) index); } } } @@ -158,12 +164,10 @@ final class BlockLight implements Light { Set toUpdate = new HashSet<>(); // Update single section with base lighting changes - Block[] blocks = new Block[SECTION_SIZE * SECTION_SIZE * SECTION_SIZE]; - IntArrayFIFOQueue queue = buildInternalQueue(blockPalette, blocks); + ShortArrayFIFOQueue queue = buildInternalQueue(blockPalette); - Result result = LightCompute.compute(blocks, queue); + Result result = LightCompute.compute(blockPalette, queue); this.content = result.light(); - this.borders = result.borders(); // Propagate changes to neighbors and self for (int i = -1; i <= 1; i++) { @@ -212,7 +216,6 @@ final class BlockLight implements Light { private void clearCache() { this.contentPropagation = null; - this.bordersPropagation = null; isValidBorders = true; needsSend = true; } @@ -226,57 +229,28 @@ final class BlockLight implements Light { return res; } - private boolean compareBorders(byte[] a, byte[] b) { - if (b == null && a == null) return true; - if (b == null || a == null) return false; - - if (a.length != b.length) return false; - for (int i = 0; i < a.length; i++) { - if (a[i] > b[i]) return false; - } - return true; - } - - private Block[] blocks() { - Block[] blocks = new Block[SECTION_SIZE * SECTION_SIZE * SECTION_SIZE]; - - blockPalette.getAllPresent((x, y, z, stateId) -> { - final Block block = Block.fromStateId((short) stateId); - assert block != null; - final int index = x | (z << 4) | (y << 8); - blocks[index] = block; - }); - - return blocks; - } - @Override public Light calculateExternal(Instance instance, Chunk chunk, int sectionY) { if (!isValidBorders) clearCache(); - Map neighbors = Light.getNeighbors(chunk, sectionY); + Point[] neighbors = Light.getNeighbors(chunk, sectionY); - Block[] blocks = blocks(); - IntArrayFIFOQueue queue = buildExternalQueue(instance, blocks, neighbors, borders); - LightCompute.Result result = LightCompute.compute(blocks, queue); + ShortArrayFIFOQueue queue = buildExternalQueue(instance, blockPalette, neighbors, content); + LightCompute.Result result = LightCompute.compute(blockPalette, queue); byte[] contentPropagationTemp = result.light(); - byte[][] borderTemp = result.borders(); this.contentPropagationSwap = bake(contentPropagationSwap, contentPropagationTemp); - this.bordersPropagationSwap = combineBorders(bordersPropagation, borderTemp); Set toUpdate = new HashSet<>(); // Propagate changes to neighbors and self - for (var entry : neighbors.entrySet()) { - var neighbor = entry.getValue(); - var face = entry.getKey(); + for (int i = 0; i < neighbors.length; i++) { + var neighbor = neighbors[i]; + if (neighbor == null) continue; + var face = BlockFace.values()[i]; - byte[] next = borderTemp[face.ordinal()]; - byte[] current = getBorderPropagation(face); - - if (!compareBorders(next, current)) { + if (!Light.compareBorders(content, contentPropagation, contentPropagationTemp, face)) { toUpdate.add(neighbor); } } @@ -285,17 +259,6 @@ final class BlockLight implements Light { return this; } - private byte[][] combineBorders(byte[][] b1, byte[][] b2) { - if (b1 == null) return b2; - - byte[][] newBorder = new byte[FACES.length][]; - Arrays.setAll(newBorder, i -> new byte[SIDE_LENGTH]); - for (int i = 0; i < FACES.length; i++) { - newBorder[i] = combineBorders(b1[i], b2[i]); - } - return newBorder; - } - private byte[] bake(byte[] content1, byte[] content2) { if (content1 == null && content2 == null) return emptyContent; if (content1 == emptyContent && content2 == emptyContent) return emptyContent; @@ -321,39 +284,18 @@ final class BlockLight implements Light { return lightMax; } - @Override - public byte[] getBorderPropagation(BlockFace face) { - if (!isValidBorders) clearCache(); - - if (borders == null && bordersPropagation == null) return new byte[SIDE_LENGTH]; - if (borders == null) return bordersPropagation[face.ordinal()]; - if (bordersPropagation == null) return borders[face.ordinal()]; - - return combineBorders(bordersPropagation[face.ordinal()], borders[face.ordinal()]); - } - @Override public void invalidatePropagation() { this.isValidBorders = false; this.needsSend = false; - this.bordersPropagation = null; this.contentPropagation = null; } @Override public int getLevel(int x, int y, int z) { - var array = array(); + if (content == null) return 0; int index = x | (z << 4) | (y << 8); - return LightCompute.getLight(array, index); - } - - private byte[] combineBorders(byte[] b1, byte[] b2) { - byte[] newBorder = new byte[SIDE_LENGTH]; - for (int i = 0; i < newBorder.length; i++) { - var previous = b2[i]; - var current = b1[i]; - newBorder[i] = (byte) Math.max(previous, current); - } - return newBorder; + if (contentPropagation == null) return LightCompute.getLight(content, index); + return Math.max(LightCompute.getLight(contentPropagation, index), LightCompute.getLight(content, index)); } } \ No newline at end of file diff --git a/src/main/java/net/minestom/server/instance/light/Light.java b/src/main/java/net/minestom/server/instance/light/Light.java index 43429c1e7..a25f38eb8 100644 --- a/src/main/java/net/minestom/server/instance/light/Light.java +++ b/src/main/java/net/minestom/server/instance/light/Light.java @@ -14,6 +14,9 @@ import java.util.HashMap; import java.util.Map; import java.util.Set; +import static net.minestom.server.instance.light.LightCompute.SECTION_SIZE; +import static net.minestom.server.instance.light.LightCompute.getLight; + public interface Light { static Light sky(@NotNull Palette blockPalette) { return new SkyLight(blockPalette); @@ -35,9 +38,6 @@ public interface Light { @ApiStatus.Internal Light calculateExternal(Instance instance, Chunk chunk, int sectionY); - @ApiStatus.Internal - byte[] getBorderPropagation(BlockFace oppositeFace); - @ApiStatus.Internal void invalidatePropagation(); @@ -53,11 +53,11 @@ public interface Light { void set(byte[] copyArray); @ApiStatus.Internal - static Map getNeighbors(Chunk chunk, int sectionY) { + static Point[] getNeighbors(Chunk chunk, int sectionY) { int chunkX = chunk.getChunkX(); int chunkZ = chunk.getChunkZ(); - Map links = new HashMap<>(); + Point[] links = new Vec[BlockFace.values().length]; for (BlockFace face : BlockFace.values()) { Direction direction = face.toDirection(); @@ -70,9 +70,41 @@ public interface Light { if (foundChunk == null) continue; if (y - foundChunk.getMinSection() > foundChunk.getMaxSection() || y - foundChunk.getMinSection() < 0) continue; - links.put(face, new Vec(foundChunk.getChunkX(), y, foundChunk.getChunkZ())); + links[face.ordinal()] = new Vec(foundChunk.getChunkX(), y, foundChunk.getChunkZ()); } return links; } + + @ApiStatus.Internal + static boolean compareBorders(byte[] content, byte[] contentPropagation, byte[] contentPropagationTemp, BlockFace face) { + if (content == null && contentPropagation == null && contentPropagationTemp == null) return true; + + final int k = switch (face) { + case WEST, BOTTOM, NORTH -> 0; + case EAST, TOP, SOUTH -> 15; + }; + + for (int bx = 0; bx < SECTION_SIZE; bx++) { + for (int by = 0; by < SECTION_SIZE; by++) { + final int posFrom = switch (face) { + case NORTH, SOUTH -> bx | (k << 4) | (by << 8); + case WEST, EAST -> k | (by << 4) | (bx << 8); + default -> bx | (by << 4) | (k << 8); + }; + + int valueFrom; + + if (content == null && contentPropagation == null) valueFrom = 0; + else if (content != null && contentPropagation == null) valueFrom = getLight(content, posFrom); + else if (content == null && contentPropagation != null) valueFrom = getLight(contentPropagation, posFrom); + else valueFrom = Math.max(getLight(content, posFrom), getLight(contentPropagation, posFrom)); + + int valueTo = getLight(contentPropagationTemp, posFrom); + + if (valueFrom < valueTo) return false; + } + } + return true; + } } diff --git a/src/main/java/net/minestom/server/instance/light/LightCompute.java b/src/main/java/net/minestom/server/instance/light/LightCompute.java index cb56ec70d..2d48bb3cd 100644 --- a/src/main/java/net/minestom/server/instance/light/LightCompute.java +++ b/src/main/java/net/minestom/server/instance/light/LightCompute.java @@ -1,13 +1,13 @@ package net.minestom.server.instance.light; -import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue; +import it.unimi.dsi.fastutil.shorts.ShortArrayFIFOQueue; import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.BlockFace; import net.minestom.server.instance.palette.Palette; import net.minestom.server.utils.Direction; import org.jetbrains.annotations.NotNull; -import java.util.LinkedList; +import java.util.ArrayDeque; import java.util.Objects; import static net.minestom.server.instance.light.BlockLight.buildInternalQueue; @@ -15,29 +15,25 @@ import static net.minestom.server.instance.light.BlockLight.buildInternalQueue; public final class LightCompute { static final BlockFace[] FACES = BlockFace.values(); static final int LIGHT_LENGTH = 16 * 16 * 16 / 2; - static final int SIDE_LENGTH = 16 * 16; static final int SECTION_SIZE = 16; - public static final byte[][] emptyBorders = new byte[FACES.length][SIDE_LENGTH]; public static final byte[] emptyContent = new byte[LIGHT_LENGTH]; static @NotNull Result compute(Palette blockPalette) { - Block[] blocks = new Block[4096]; - return LightCompute.compute(blocks, buildInternalQueue(blockPalette, blocks)); + return LightCompute.compute(blockPalette, buildInternalQueue(blockPalette)); } - static @NotNull Result compute(Block[] blocks, IntArrayFIFOQueue lightPre) { + static @NotNull Result compute(Palette blockPalette, ShortArrayFIFOQueue lightPre) { if (lightPre.isEmpty()) { - return new Result(emptyContent, emptyBorders); + return new Result(emptyContent); } - byte[][] borders = new byte[FACES.length][SIDE_LENGTH]; byte[] lightArray = new byte[LIGHT_LENGTH]; - var lightSources = new LinkedList(); + var lightSources = new ArrayDeque(); while (!lightPre.isEmpty()) { - int index = lightPre.dequeueInt(); + int index = lightPre.dequeueShort(); final int x = index & 15; final int z = (index >> 4) & 15; @@ -49,7 +45,7 @@ public final class LightCompute { if (oldLightLevel < newLightLevel) { placeLight(lightArray, newIndex, newLightLevel); - lightSources.add(index); + lightSources.add((short) index); } } @@ -66,34 +62,29 @@ public final class LightCompute { final int yO = y + dir.normalY(); final int zO = z + dir.normalZ(); final byte newLightLevel = (byte) (lightLevel - 1); + // Handler border if (xO < 0 || xO >= SECTION_SIZE || yO < 0 || yO >= SECTION_SIZE || zO < 0 || zO >= SECTION_SIZE) { - final byte[] border = borders[face.ordinal()]; - final int borderIndex = switch (face) { - case WEST, EAST -> y * SECTION_SIZE + z; - case BOTTOM, TOP -> x * SECTION_SIZE + z; - case NORTH, SOUTH -> x * SECTION_SIZE + y; - }; - border[borderIndex] = newLightLevel; continue; } + // Section final int newIndex = xO | (zO << 4) | (yO << 8); if (getLight(lightArray, newIndex) + 2 <= lightLevel) { - final Block currentBlock = Objects.requireNonNullElse(blocks[x | (z << 4) | (y << 8)], Block.AIR); + final Block currentBlock = Objects.requireNonNullElse(Block.fromStateId((short)blockPalette.get(x, y, z)), Block.AIR); + final Block propagatedBlock = Objects.requireNonNullElse(Block.fromStateId((short)blockPalette.get(xO, yO, zO)), Block.AIR); - final Block propagatedBlock = Objects.requireNonNullElse(blocks[newIndex], Block.AIR); boolean airAir = currentBlock.isAir() && propagatedBlock.isAir(); if (!airAir && currentBlock.registry().collisionShape().isOccluded(propagatedBlock.registry().collisionShape(), face)) continue; placeLight(lightArray, newIndex, newLightLevel); - lightSources.add(newIndex | (newLightLevel << 12)); + lightSources.add((short) (newIndex | (newLightLevel << 12))); } } } - return new Result(lightArray, borders); + return new Result(lightArray); } - record Result(byte[] light, byte[][] borders) { + record Result(byte[] light) { Result { assert light.length == LIGHT_LENGTH : "Only 16x16x16 sections are supported: " + light.length; } diff --git a/src/main/java/net/minestom/server/instance/light/SkyLight.java b/src/main/java/net/minestom/server/instance/light/SkyLight.java index 7f7ef160e..dd435dc58 100644 --- a/src/main/java/net/minestom/server/instance/light/SkyLight.java +++ b/src/main/java/net/minestom/server/instance/light/SkyLight.java @@ -1,6 +1,6 @@ package net.minestom.server.instance.light; -import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue; +import it.unimi.dsi.fastutil.shorts.ShortArrayFIFOQueue; import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Vec; import net.minestom.server.instance.Chunk; @@ -16,6 +16,7 @@ import java.util.Arrays; import java.util.HashSet; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import static net.minestom.server.instance.light.LightCompute.*; @@ -26,23 +27,17 @@ final class SkyLight implements Light { private byte[] contentPropagation; private byte[] contentPropagationSwap; - private byte[][] borders; - private byte[][] bordersPropagation; - private byte[][] bordersPropagationSwap; private boolean isValidBorders = true; private boolean needsSend = true; private Set toUpdateSet = new HashSet<>(); + private final Section[] neighborSections = new Section[BlockFace.values().length]; private boolean fullyLit = false; - private static final byte[][] bordersFullyLit = new byte[6][SIDE_LENGTH]; private static final byte[] contentFullyLit = new byte[LIGHT_LENGTH]; static { Arrays.fill(contentFullyLit, (byte) -1); - for (byte[] border : bordersFullyLit) { - Arrays.fill(border, (byte) 14); - } } SkyLight(Palette blockPalette) { @@ -51,21 +46,17 @@ final class SkyLight implements Light { @Override public Set flip() { - if (this.bordersPropagationSwap != null) - this.bordersPropagation = this.bordersPropagationSwap; - if (this.contentPropagationSwap != null) this.contentPropagation = this.contentPropagationSwap; - this.bordersPropagationSwap = null; this.contentPropagationSwap = null; if (toUpdateSet == null) return Set.of(); return toUpdateSet; } - static IntArrayFIFOQueue buildInternalQueue(Chunk c, int sectionY) { - IntArrayFIFOQueue lightSources = new IntArrayFIFOQueue(); + static ShortArrayFIFOQueue buildInternalQueue(Chunk c, int sectionY) { + ShortArrayFIFOQueue lightSources = new ShortArrayFIFOQueue(); if (c instanceof LightingChunk lc) { int[] heightmap = lc.calculateHeightMap(); @@ -79,7 +70,7 @@ final class SkyLight implements Light { for (int y = Math.min(sectionMaxY, maxY); y >= Math.max(height, sectionMinY); y--) { int index = x | (z << 4) | ((y % 16) << 8); - lightSources.enqueue(index | (15 << 12)); + lightSources.enqueue((short) (index | (15 << 12))); } } } @@ -92,45 +83,55 @@ final class SkyLight implements Light { return Block.fromStateId((short)palette.get(x, y, z)); } - private static IntArrayFIFOQueue buildExternalQueue(Instance instance, Block[] blocks, Map neighbors, byte[][] borders) { - IntArrayFIFOQueue lightSources = new IntArrayFIFOQueue(); + private ShortArrayFIFOQueue buildExternalQueue(Instance instance, Palette blockPalette, Point[] neighbors, byte[] content) { + ShortArrayFIFOQueue lightSources = new ShortArrayFIFOQueue(); - for (BlockFace face : BlockFace.values()) { - Point neighborSection = neighbors.get(face); + for (int i = 0; i < neighbors.length; i++) { + var face = BlockFace.values()[i]; + Point neighborSection = neighbors[i]; if (neighborSection == null) continue; - Chunk chunk = instance.getChunk(neighborSection.blockX(), neighborSection.blockZ()); - if (chunk == null) continue; + Section otherSection = neighborSections[face.ordinal()]; - byte[] neighborFace = chunk.getSection(neighborSection.blockY()).skyLight().getBorderPropagation(face.getOppositeFace()); - if (neighborFace == null) continue; + if (otherSection == null) { + Chunk chunk = instance.getChunk(neighborSection.blockX(), neighborSection.blockZ()); + if (chunk == null) continue; + + otherSection = chunk.getSection(neighborSection.blockY()); + neighborSections[face.ordinal()] = otherSection; + } + + var otherLight = otherSection.skyLight(); for (int bx = 0; bx < 16; bx++) { for (int by = 0; by < 16; by++) { - final int borderIndex = bx * SECTION_SIZE + by; - byte lightEmission = neighborFace[borderIndex]; - - if (borders != null && borders[face.ordinal()] != null) { - final int internalEmission = borders[face.ordinal()][borderIndex]; - if (lightEmission <= internalEmission) continue; - } - - if (borders != null && borders[face.ordinal()] != null) { - final int internalEmission = borders[face.ordinal()][borderIndex]; - if (lightEmission <= internalEmission) continue; - } final int k = switch (face) { case WEST, BOTTOM, NORTH -> 0; case EAST, TOP, SOUTH -> 15; }; + final byte lightEmission = (byte) Math.max(switch (face) { + case NORTH, SOUTH -> (byte) otherLight.getLevel(bx, by, 15 - k); + case WEST, EAST -> (byte) otherLight.getLevel(15 - k, bx, by); + default -> (byte) otherLight.getLevel(bx, 15 - k, by); + } - 1, 0); + final int posTo = switch (face) { case NORTH, SOUTH -> bx | (k << 4) | (by << 8); case WEST, EAST -> k | (by << 4) | (bx << 8); default -> bx | (by << 4) | (k << 8); }; - Section otherSection = chunk.getSection(neighborSection.blockY()); + if (content != null) { + final int internalEmission = (byte) (Math.max(getLight(content, posTo) - 1, 0)); + if (lightEmission <= internalEmission) continue; + } + + final Block blockTo = switch (face) { + case NORTH, SOUTH -> getBlock(blockPalette, bx, by, k); + case WEST, EAST -> getBlock(blockPalette, k, bx, by); + default -> getBlock(blockPalette, bx, k, by); + }; final Block blockFrom = (switch (face) { case NORTH, SOUTH -> getBlock(otherSection.blockPalette(), bx, by, 15 - k); @@ -138,9 +139,6 @@ final class SkyLight implements Light { default -> getBlock(otherSection.blockPalette(), bx, 15 - k, by); }); - if (blocks == null) continue; - Block blockTo = blocks[posTo]; - if (blockTo == null && blockFrom != null) { if (blockFrom.registry().collisionShape().isOccluded(Block.AIR.registry().collisionShape(), face.getOppositeFace())) continue; @@ -155,7 +153,7 @@ final class SkyLight implements Light { final int index = posTo | (lightEmission << 12); if (lightEmission > 0) { - lightSources.enqueue(index); + lightSources.enqueue((short) index); } } } @@ -173,13 +171,15 @@ final class SkyLight implements Light { @Override public Light calculateInternal(Instance instance, int chunkX, int sectionY, int chunkZ) { Chunk chunk = instance.getChunk(chunkX, chunkZ); + if (chunk == null) { + this.toUpdateSet = Set.of(); + return this; + } this.isValidBorders = true; // Update single section with base lighting changes - Block[] blocks = blocks(); - int queueSize = SECTION_SIZE * SECTION_SIZE * SECTION_SIZE; - IntArrayFIFOQueue queue = new IntArrayFIFOQueue(0); + ShortArrayFIFOQueue queue = new ShortArrayFIFOQueue(0); if (!fullyLit) { queue = buildInternalQueue(chunk, sectionY); queueSize = queue.size(); @@ -188,11 +188,9 @@ final class SkyLight implements Light { if (queueSize == SECTION_SIZE * SECTION_SIZE * SECTION_SIZE) { this.fullyLit = true; this.content = contentFullyLit; - this.borders = bordersFullyLit; } else { - Result result = LightCompute.compute(blocks, queue); + Result result = LightCompute.compute(blockPalette, queue); this.content = result.light(); - this.borders = result.borders(); } Set toUpdate = new HashSet<>(); @@ -244,7 +242,6 @@ final class SkyLight implements Light { private void clearCache() { this.contentPropagation = null; - this.bordersPropagation = null; isValidBorders = true; needsSend = true; fullyLit = false; @@ -259,63 +256,35 @@ final class SkyLight implements Light { return res; } - private boolean compareBorders(byte[] a, byte[] b) { - if (b == null && a == null) return true; - if (b == null || a == null) return false; - - if (a.length != b.length) return false; - for (int i = 0; i < a.length; i++) { - if (a[i] > b[i]) return false; - } - return true; - } - - private Block[] blocks() { - Block[] blocks = new Block[SECTION_SIZE * SECTION_SIZE * SECTION_SIZE]; - - blockPalette.getAllPresent((x, y, z, stateId) -> { - final Block block = Block.fromStateId((short) stateId); - assert block != null; - final int index = x | (z << 4) | (y << 8); - blocks[index] = block; - }); - - return blocks; - } - @Override public Light calculateExternal(Instance instance, Chunk chunk, int sectionY) { if (!isValidBorders) clearCache(); - Map neighbors = Light.getNeighbors(chunk, sectionY); + Point[] neighbors = Light.getNeighbors(chunk, sectionY); Set toUpdate = new HashSet<>(); - Block[] blocks = blocks(); - IntArrayFIFOQueue queue; + ShortArrayFIFOQueue queue; + + byte[] contentPropagationTemp = contentFullyLit; - byte[][] borderTemp = bordersFullyLit; if (!fullyLit) { - queue = buildExternalQueue(instance, blocks, neighbors, borders); - LightCompute.Result result = LightCompute.compute(blocks, queue); + queue = buildExternalQueue(instance, blockPalette, neighbors, content); + LightCompute.Result result = LightCompute.compute(blockPalette, queue); - byte[] contentPropagationTemp = result.light(); - borderTemp = result.borders(); + contentPropagationTemp = result.light(); this.contentPropagationSwap = bake(contentPropagationSwap, contentPropagationTemp); - this.bordersPropagationSwap = combineBorders(bordersPropagation, borderTemp); } else { this.contentPropagationSwap = null; - this.bordersPropagationSwap = null; } // Propagate changes to neighbors and self - for (var entry : neighbors.entrySet()) { - var neighbor = entry.getValue(); - var face = entry.getKey(); + for (int i = 0; i < neighbors.length; i++) { + var neighbor = neighbors[i]; + if (neighbor == null) continue; - byte[] next = borderTemp[face.ordinal()]; - byte[] current = getBorderPropagation(face); + var face = BlockFace.values()[i]; - if (!compareBorders(next, current)) { + if (!Light.compareBorders(content, contentPropagation, contentPropagationTemp, face)) { toUpdate.add(neighbor); } } @@ -324,17 +293,6 @@ final class SkyLight implements Light { return this; } - private byte[][] combineBorders(byte[][] b1, byte[][] b2) { - if (b1 == null) return b2; - - byte[][] newBorder = new byte[FACES.length][]; - Arrays.setAll(newBorder, i -> new byte[SIDE_LENGTH]); - for (int i = 0; i < FACES.length; i++) { - newBorder[i] = combineBorders(b1[i], b2[i]); - } - return newBorder; - } - private byte[] bake(byte[] content1, byte[] content2) { if (content1 == null && content2 == null) return emptyContent; if (content1 == emptyContent && content2 == emptyContent) return emptyContent; @@ -360,39 +318,18 @@ final class SkyLight implements Light { return lightMax; } - @Override - public byte[] getBorderPropagation(BlockFace face) { - if (!isValidBorders) clearCache(); - - if (borders == null && bordersPropagation == null) return new byte[SIDE_LENGTH]; - if (borders == null) return bordersPropagation[face.ordinal()]; - if (bordersPropagation == null) return borders[face.ordinal()]; - - return combineBorders(bordersPropagation[face.ordinal()], borders[face.ordinal()]); - } - @Override public void invalidatePropagation() { this.isValidBorders = false; this.needsSend = false; - this.bordersPropagation = null; this.contentPropagation = null; } @Override public int getLevel(int x, int y, int z) { - var array = array(); + if (content == null) return 0; int index = x | (z << 4) | (y << 8); - return LightCompute.getLight(array, index); - } - - private byte[] combineBorders(byte[] b1, byte[] b2) { - byte[] newBorder = new byte[SIDE_LENGTH]; - for (int i = 0; i < newBorder.length; i++) { - var previous = b2[i]; - var current = b1[i]; - newBorder[i] = (byte) Math.max(previous, current); - } - return newBorder; + if (contentPropagation == null) return LightCompute.getLight(content, index); + return Math.max(LightCompute.getLight(contentPropagation, index), LightCompute.getLight(content, index)); } } \ No newline at end of file diff --git a/src/test/java/net/minestom/server/instance/light/LightParityIntegrationTest.java b/src/test/java/net/minestom/server/instance/light/LightParityIntegrationTest.java index 466a0b5c1..7da0a1ac7 100644 --- a/src/test/java/net/minestom/server/instance/light/LightParityIntegrationTest.java +++ b/src/test/java/net/minestom/server/instance/light/LightParityIntegrationTest.java @@ -19,12 +19,13 @@ import java.net.URISyntaxException; import java.net.URL; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.CompletableFuture; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assumptions.assumeTrue; @EnvTest public class LightParityIntegrationTest { + private static final int REGION_SIZE = 3; @Test public void test(Env env) throws URISyntaxException, IOException, AnvilException { @@ -35,14 +36,20 @@ public class LightParityIntegrationTest { instance.setChunkSupplier(LightingChunk::new); instance.setChunkLoader(new AnvilLoader(Path.of("./src/test/resources/net/minestom/server/instance/lighting"))); - int end = 4; + List> futures = new ArrayList<>(); + + int end = REGION_SIZE; // Load the chunks for (int x = 0; x < end; x++) { for (int z = 0; z < end; z++) { - instance.loadChunk(x, z).join(); + futures.add(instance.loadChunk(x, z)); } } + for (CompletableFuture future : futures) { + future.join(); + } + LightingChunk.relight(instance, instance.getChunks()); int differences = 0; @@ -60,7 +67,7 @@ public class LightParityIntegrationTest { } for (int sectionIndex = chunk.getMinSection(); sectionIndex < chunk.getMaxSection(); sectionIndex++) { - if (sectionIndex != 3) continue; + if (sectionIndex > 6) break; Section section = chunk.getSection(sectionIndex); @@ -93,6 +100,7 @@ public class LightParityIntegrationTest { } } + // Mojang's sky lighting is wrong { int serverSkyValue = LightCompute.getLight(serverSky, index); int mcaSkyValue = mcaSky.length == 0 ? 0 : LightCompute.getLight(mcaSky, index); @@ -109,10 +117,10 @@ public class LightParityIntegrationTest { } } - assertEquals(0, differences); - assertEquals(0, differencesZero); assertEquals(0, blocks); assertEquals(0, sky); + assertEquals(0, differences); + assertEquals(0, differencesZero); } record SectionEntry(Palette blocks, byte[] sky, byte[] block) { @@ -127,8 +135,8 @@ public class LightParityIntegrationTest { Map sections = new HashMap<>(); // Read from anvil - for (int x = 1; x < 3; x++) { - for (int z = 1; z < 3; z++) { + for (int x = 1; x < REGION_SIZE - 1; x++) { + for (int z = 1; z < REGION_SIZE - 1; z++) { var chunk = regionFile.getChunk(x, z); if (chunk == null) continue;