From 97b86d69c4031bf575280e8b3e30b8e3470598d9 Mon Sep 17 00:00:00 2001 From: James Seibel Date: Tue, 11 Jun 2024 18:34:57 -0500 Subject: [PATCH] Move shared ChunkWrapper code form Main to Core --- .../core/generation/DhLightingEngine.java | 6 +- .../core/util/gridList/ArrayGridList.java | 13 + .../chunk/ChunkLightStorage.java | 257 ++++++++++++++++++ .../chunk/IChunkWrapper.java | 69 ++++- 4 files changed, 340 insertions(+), 5 deletions(-) create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/chunk/ChunkLightStorage.java diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/DhLightingEngine.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/DhLightingEngine.java index 0e648ef33..6648f62ee 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/DhLightingEngine.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/DhLightingEngine.java @@ -33,6 +33,7 @@ import java.util.*; import java.util.concurrent.locks.ReentrantLock; import it.unimi.dsi.fastutil.ints.IntArrayList; +import org.jetbrains.annotations.NotNull; /** * This logic was roughly based on @@ -67,7 +68,7 @@ public class DhLightingEngine * @param nearbyChunkList should also contain centerChunk * @param maxSkyLight should be a value between 0 and 15 */ - public void lightChunk(IChunkWrapper centerChunk, ArrayList nearbyChunkList, int maxSkyLight) + public void lightChunk(@NotNull IChunkWrapper centerChunk, @NotNull ArrayList nearbyChunkList, int maxSkyLight) { DhChunkPos centerChunkPos = centerChunk.getChunkPos(); AdjacentChunkHolder adjacentChunkHolder = new AdjacentChunkHolder(centerChunk); @@ -104,7 +105,6 @@ public class DhLightingEngine for (int chunkIndex = 0; chunkIndex < nearbyChunkList.size(); chunkIndex++) // using iterators in high traffic areas can cause GC issues due to allocating a bunch of iterators, use an indexed for-loop instead { IChunkWrapper chunk = nearbyChunkList.get(chunkIndex); - if (chunk != null && requestedAdjacentPositions.contains(chunk.getChunkPos())) { // remove the newly found position @@ -179,7 +179,7 @@ public class DhLightingEngine break; } } - + // block light this.propagateLightPosList(blockLightPosQueue, adjacentChunkHolder, (neighbourChunk, relBlockPos) -> neighbourChunk.getDhBlockLight(relBlockPos.x, relBlockPos.y, relBlockPos.z), diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/gridList/ArrayGridList.java b/core/src/main/java/com/seibel/distanthorizons/core/util/gridList/ArrayGridList.java index 1d0def119..e6c889a63 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/gridList/ArrayGridList.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/gridList/ArrayGridList.java @@ -30,6 +30,13 @@ public class ArrayGridList extends ArrayList { public final int gridSize; + + + //==============// + // constructors // + //==============// + + /** @param filler the function called for each index to set the initial values */ public ArrayGridList(int gridSize, BiFunction filler) { super((gridSize) * (gridSize)); @@ -67,6 +74,12 @@ public class ArrayGridList extends ArrayList // "==========================================\n"); } + + + //=========// + // methods // + //=========// + protected int _indexOf(int x, int y) { return x + y * gridSize; diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/chunk/ChunkLightStorage.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/chunk/ChunkLightStorage.java new file mode 100644 index 000000000..73ced41be --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/chunk/ChunkLightStorage.java @@ -0,0 +1,257 @@ +/* + * This file is part of the Distant Horizons mod + * licensed under the GNU LGPL v3 License. + * + * Copyright (C) 2020-2023 James Seibel + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.seibel.distanthorizons.core.wrapperInterfaces.chunk; + +import com.seibel.distanthorizons.core.util.LodUtil; +import com.seibel.distanthorizons.coreapi.util.BitShiftUtil; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Compact, efficient storage for light levels. + * all blocks only take up 4 bits in total, + * and if a 16x16x16 area is detected to have the same light level in all positions, + * then we store a single byte for that light level, instead of 2 kilobytes. + * + * @author Builderb0y +*/ +public class ChunkLightStorage +{ + /** the minimum Y level in the chunk which this storage is storing light levels for (inclusive). */ + public int minY; + /** the maximum Y level in the chunk which this storage is storing light levels for (exclusive). */ + public int maxY; + + /** the data stored in this storage, split up into 16x16x16 areas. */ + public LightSection[] lightSections; + + /** + * If the get method is called on a Y position above what's stored + * this value will be returned.

+ * + * This needs to be manually defined since sky and block lights behave differently + * for values both above and below what's defined. + */ + public int aboveMaxYValue; + /** @see ChunkLightStorage#aboveMaxYValue */ + public int belowMinYValue; + + + + //=============// + // constructor // + //=============// + + public static ChunkLightStorage createSkyLightStorage(IChunkWrapper chunkWrapper) { return createSkyLightStorage(chunkWrapper.getMinBuildHeight(), chunkWrapper.getMaxBuildHeight()); } + public static ChunkLightStorage createSkyLightStorage(int minY, int maxY) + { + return new ChunkLightStorage( + minY, maxY, + // positions above should be lit but positions below should be unlit + LodUtil.MAX_MC_LIGHT, LodUtil.MIN_MC_LIGHT); + } + public static ChunkLightStorage createBlockLightStorage(IChunkWrapper chunkWrapper) { return createBlockLightStorage(chunkWrapper.getMinBuildHeight(), chunkWrapper.getMaxBuildHeight()); } + public static ChunkLightStorage createBlockLightStorage(int minY, int maxY) + { + return new ChunkLightStorage( + minY, maxY, + // positions above and below the handled area should be unlit + LodUtil.MIN_MC_LIGHT, LodUtil.MIN_MC_LIGHT); + } + + public ChunkLightStorage(int minY, int maxY, int aboveMaxYValue, int belowMinYValue) + { + this.minY = minY; + this.maxY = maxY; + + this.aboveMaxYValue = aboveMaxYValue; + this.belowMinYValue = belowMinYValue; + } + + + + //=====================// + // getters and setters // + //=====================// + + public int get(int x, int y, int z) + { + if (y < this.minY) + { + return this.belowMinYValue; + } + else if (y >= this.maxY) + { + return this.aboveMaxYValue; + } + + + if (this.lightSections != null) + { + LightSection lightSection = this.lightSections[BitShiftUtil.divideByPowerOfTwo(y - this.minY, 4)]; + if (lightSection != null) + { + return lightSection.get(x, y, z); + } + } + + return 0; + } + + public void set(int x, int y, int z, int lightLevel) + { + if (y < this.minY || y >= this.maxY) + { + return; + } + + //populate array if it doesn't exist. + if (this.lightSections == null) + { + this.lightSections = new LightSection[BitShiftUtil.divideByPowerOfTwo(this.maxY - this.minY, 4)]; + } + + int index = (y - this.minY) >> 4; + LightSection lightSection = this.lightSections[index]; + + //populate lightSection in array if it doesn't exist. + if (lightSection == null) + { + lightSection = new LightSection(0); + this.lightSections[index] = lightSection; + } + lightSection.set(x, y, z, lightLevel); + } + + + + //================// + // helper classes // + //================// + + public static class LightSection + { + public byte constantValue; + public long[] data; + public short[] counts; + + public LightSection(int initialValue) + { + this.constantValue = (byte) (initialValue); + this.counts = new short[16]; + this.counts[initialValue] = 16 * 16 * 16; + } + + public int get(int x, int y, int z) + { + if (this.constantValue >= 0) + { + return this.constantValue; + } + + x &= 15; + y &= 15; + z &= 15; + long bits = this.data[(z << 4) | x]; + return ((int) (bits >>> (y << 2))) & 15; + } + + public void set(int x, int y, int z, int lightLevel) + { + int oldLightLevel = -1; + if (this.constantValue >= 0) + { + oldLightLevel = this.constantValue; + + //if the light level didn't change, then there's nothing to do. + if (oldLightLevel == lightLevel) return; + + //if we are a constant value and need to change something, + //then that means we need to convert to a non-constant value. + this.data = DataRecycler.get(); + + //repeat oldLightLevel 16 times as a bit pattern. + long payload = oldLightLevel; + payload |= payload << 4; + payload |= payload << 8; + payload |= payload << 16; + payload |= payload << 32; + + //fill our data with our constant value. + Arrays.fill(this.data, payload); + + //we are no longer a constant value. + this.constantValue = -1; + } + + x &= 15; + y &= 15; + z &= 15; + int index = (z << 4) | x; + long bits = this.data[index]; + //if we weren't a constant value before, now's the time to initialize oldLightLevel. + if (oldLightLevel < 0) + { + oldLightLevel = ((int) (bits >>> (y << 2))) & 15; + } + //clear the 4 bits that correspond to the light level at x, y, z... + bits &= ~(15L << (y << 2)); + //...and then re-populate those bits with the new light level. + bits |= ((long) (lightLevel)) << (y << 2); + //store the updated bits in our data. + this.data[index] = bits; + + //we have one less of the old light level... + this.counts[oldLightLevel]--; + //...and one more of the new level. + //if the number associated with the new level is now 4096 (AKA 16 ^ 3), + //then this implies every position in this section has the same light level, + //and therefore we can convert back to a constant value. + if (++this.counts[lightLevel] == 4096) + { + this.constantValue = (byte) (lightLevel); + DataRecycler.reclaim(this.data); + this.data = null; + } + } + + } + + static class DataRecycler + { + private static final ArrayList recycled = new ArrayList<>(256); + + static synchronized long[] get() + { + if (recycled.isEmpty()) + { + return new long[256]; + } + else + { + return recycled.remove(recycled.size() - 1); + } + } + + static synchronized void reclaim(long[] data) { if (recycled.size() < 256) recycled.add(data); } + } + +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/chunk/IChunkWrapper.java b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/chunk/IChunkWrapper.java index c85c1d2b7..edfabde7f 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/chunk/IChunkWrapper.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/wrapperInterfaces/chunk/IChunkWrapper.java @@ -23,6 +23,7 @@ import com.seibel.distanthorizons.core.pos.DhBlockPos; import com.seibel.distanthorizons.core.pos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; +import com.seibel.distanthorizons.coreapi.ModInfo; import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable; import com.seibel.distanthorizons.core.util.LodUtil; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; @@ -31,6 +32,11 @@ import java.util.ArrayList; public interface IChunkWrapper extends IBindable { + /** useful for debugging, but can slow down chunk operations quite a bit due to being called every time. */ + boolean RUN_RELATIVE_POS_INDEX_VALIDATION = ModInfo.IS_DEV_BUILD; + + + DhChunkPos getChunkPos(); default int getHeight() { return this.getMaxBuildHeight() - this.getMinBuildHeight(); } @@ -61,8 +67,6 @@ public interface IChunkWrapper extends IBindable int getMinBlockX(); int getMinBlockZ(); - long getLongChunkPos(); - void setIsDhLightCorrect(boolean isDhLightCorrect); void setUseDhLighting(boolean useDhLighting); boolean isLightCorrect(); @@ -153,4 +157,65 @@ public interface IChunkWrapper extends IBindable boolean isStillValid(); + + + //================// + // helper methods // + //================// + + /** used to prevent accidentally attempting to get/set values outside this chunk's boundaries */ + default void throwIndexOutOfBoundsIfRelativePosOutsideChunkBounds(int x, int y, int z) throws IndexOutOfBoundsException + { + if (!RUN_RELATIVE_POS_INDEX_VALIDATION) + { + return; + } + + + // FIXME +1 is to handle the fact that LodDataBuilder adds +1 to all block lighting calculations, also done in the constructor + int minHeight = this.getMinBuildHeight(); + int maxHeight = this.getMaxBuildHeight() + 1; + + if (x < 0 || x >= LodUtil.CHUNK_WIDTH + || z < 0 || z >= LodUtil.CHUNK_WIDTH + || y < minHeight || y > maxHeight) + { + String errorMessage = "Relative position [" + x + "," + y + "," + z + "] out of bounds. \n" + + "X/Z must be between 0 and 15 (inclusive) \n" + + "Y must be between [" + minHeight + "] and [" + maxHeight + "] (inclusive)."; + throw new IndexOutOfBoundsException(errorMessage); + } + } + + + /** + * Converts a 3D position into a 1D array index.

+ * + * Source:
+ * stackoverflow + */ + default int relativeBlockPosToIndex(int xRel, int y, int zRel) + { + int yRel = y - this.getMinBuildHeight(); + return (zRel * LodUtil.CHUNK_WIDTH * this.getHeight()) + (yRel * LodUtil.CHUNK_WIDTH) + xRel; + } + + /** + * Converts a 3D position into a 1D array index.

+ * + * Source:
+ * stackoverflow + */ + default DhBlockPos indexToRelativeBlockPos(int index) + { + final int zRel = index / (LodUtil.CHUNK_WIDTH * this.getHeight()); + index -= (zRel * LodUtil.CHUNK_WIDTH * this.getHeight()); + + final int y = index / LodUtil.CHUNK_WIDTH; + final int yRel = y + this.getMinBuildHeight(); + + final int xRel = index % LodUtil.CHUNK_WIDTH; + return new DhBlockPos(xRel, yRel, zRel); + } + }