diff --git a/_Misc Files/test files/Lighting engine test chunk data.7z b/_Misc Files/test files/Lighting engine test chunk data.7z new file mode 100644 index 000000000..09a8dda5f Binary files /dev/null and b/_Misc Files/test files/Lighting engine test chunk data.7z differ diff --git a/core/src/test/java/testItems/lightingEngine/LightingTestBlockStateWrapper.java b/core/src/test/java/testItems/lightingEngine/LightingTestBlockStateWrapper.java new file mode 100644 index 000000000..1ecea8ab5 --- /dev/null +++ b/core/src/test/java/testItems/lightingEngine/LightingTestBlockStateWrapper.java @@ -0,0 +1,107 @@ +/* + * 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 testItems.lightingEngine; + +import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; +import tests.LightingEngineTest; + +/** + * @see LightingEngineTest + * @see LightingTestChunkWrapper + */ +public class LightingTestBlockStateWrapper implements IBlockStateWrapper +{ + private int opacity = -1; + private int lightEmission = -1; + + + + //=============// + // constructor // + //=============// + + public LightingTestBlockStateWrapper(int opacity, int lightEmission) + { + this.opacity = opacity; + this.lightEmission = lightEmission; + } + + + + //=================// + // wrapper methods // + //=================// + + @Override + public int getOpacity() { return this.opacity; } + + @Override + public int getLightEmission() { return this.lightEmission; } + + + + + //===============// + // unimplemented // + //===============// + + //@Override + //public boolean equals(Object obj) + //{ + // if (this == obj) + // { + // return true; + // } + // + // if (obj == null || this.getClass() != obj.getClass()) + // { + // return false; + // } + // + // BlockStateTestWrapper that = (BlockStateTestWrapper) obj; + // // the serialized value is used so we can test the contents instead of the references + // return Objects.equals(this.getSerialString(), that.getSerialString()); + //} + + //@Override + //public int hashCode() { return this.hashCode; } + //@Override + //public String toString() { return this.getSerialString(); } + + + @Override + public String getSerialString() { throw new UnsupportedOperationException("Not Implemented"); } + + @Override + public Object getWrappedMcObject() { throw new UnsupportedOperationException("Not Implemented"); } + + @Override + public boolean isAir() { throw new UnsupportedOperationException("Not Implemented"); } + + @Override + public boolean isSolid() { throw new UnsupportedOperationException("Not Implemented"); } + + @Override + public boolean isLiquid() { throw new UnsupportedOperationException("Not Implemented"); } + + @Override + public byte getIrisBlockMaterialId() { throw new UnsupportedOperationException("Not Implemented"); } + +} diff --git a/core/src/test/java/testItems/lightingEngine/LightingTestChunkWrapper.java b/core/src/test/java/testItems/lightingEngine/LightingTestChunkWrapper.java new file mode 100644 index 000000000..30be0d596 --- /dev/null +++ b/core/src/test/java/testItems/lightingEngine/LightingTestChunkWrapper.java @@ -0,0 +1,421 @@ +/* + * 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 testItems.lightingEngine; + +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.pos.DhBlockPos; +import com.seibel.distanthorizons.core.pos.DhChunkPos; +import com.seibel.distanthorizons.core.util.LodUtil; +import com.seibel.distanthorizons.core.util.objects.DataCorruptedException; +import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.ChunkLightStorage; +import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; +import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap; +import org.apache.logging.log4j.Logger; +import tests.LightingEngineTest; + +import java.io.*; +import java.util.ArrayList; + +/** + * @see LightingEngineTest + * @see LightingTestBlockStateWrapper + */ +public class LightingTestChunkWrapper implements IChunkWrapper +{ + private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + + + // chunk values // + + private final DhChunkPos chunkPos; + private ChunkLightStorage blockLightStorage; + private ChunkLightStorage skyLightStorage; + + private ArrayList blockLightPosList = null; + + private boolean useDhLighting; + + private int minNonEmptyHeight = Integer.MIN_VALUE; + private int maxNonEmptyHeight = Integer.MAX_VALUE; + + + // test values // + + private final Int2IntOpenHashMap blockOpacityStorage; + private final Int2IntOpenHashMap blockEmissionStorage; + private final int[][] solidHeightMap = new int[LodUtil.CHUNK_WIDTH][LodUtil.CHUNK_WIDTH]; + private final int[][] lightBlockingHeightMap = new int[LodUtil.CHUNK_WIDTH][LodUtil.CHUNK_WIDTH]; + + + + + //=============// + // constructor // + //=============// + + public LightingTestChunkWrapper(IChunkWrapper chunkWrapper) + { + this.chunkPos = chunkWrapper.getChunkPos(); + + this.blockOpacityStorage = new Int2IntOpenHashMap(); + this.blockEmissionStorage = new Int2IntOpenHashMap(); + this.blockLightPosList = new ArrayList<>(); + + for (int x = 0; x < LodUtil.CHUNK_WIDTH; x++) + { + for (int z = 0; z < LodUtil.CHUNK_WIDTH; z++) + { + for (int y = this.getMinBuildHeight(); y < this.getMaxBuildHeight(); y++) + { + IBlockStateWrapper block = chunkWrapper.getBlockState(x,y,z); + + int opacity = block.getOpacity(); + if (opacity >= IBlockStateWrapper.FULLY_OPAQUE) + { + opacity = 3; + } + + this.blockOpacityStorage.put(new DhBlockPos(x, y, z).hashCode(), opacity); + this.blockEmissionStorage.put(new DhBlockPos(x, y, z).hashCode(), block.getLightEmission()); + + if (block.getLightEmission() != 0) + { + this.blockLightPosList.add(new DhBlockPos(x,y,z)); + } + } + + this.lightBlockingHeightMap[x][z] = chunkWrapper.getLightBlockingHeightMapValue(x, z); + this.solidHeightMap[x][z] = chunkWrapper.getSolidHeightMapValue(x, z); + + } + } + } + + + + //===============// + // file handling // + //===============// + + public LightingTestChunkWrapper(DhChunkPos pos, File saveFile) throws DataCorruptedException + { + this.chunkPos = pos; + + this.blockOpacityStorage = new Int2IntOpenHashMap(); + this.blockEmissionStorage = new Int2IntOpenHashMap(); + this.blockLightPosList = new ArrayList<>(); + + try(FileInputStream inputStream = new FileInputStream(saveFile); + BufferedInputStream bufferedStream = new BufferedInputStream(inputStream); + DataInputStream stream = new DataInputStream(bufferedStream)) + { + for (int x = 0; x < LodUtil.CHUNK_WIDTH; x++) + { + for (int z = 0; z < LodUtil.CHUNK_WIDTH; z++) + { + for (int y = this.getMinBuildHeight(); y < this.getMaxBuildHeight(); y++) + { + this.blockOpacityStorage.put(new DhBlockPos(x, y, z).hashCode(), stream.readInt()); + + int blockEmission = stream.readInt(); + this.blockEmissionStorage.put(new DhBlockPos(x, y, z).hashCode(), blockEmission); + if (blockEmission != 0) + { + this.blockLightPosList.add(new DhBlockPos(x,y,z)); + } + } + + if (stream.readChar() != ';') + { + throw new DataCorruptedException("bad height map"); + } + + this.lightBlockingHeightMap[x][z] = stream.readInt(); + this.solidHeightMap[x][z] = stream.readInt(); + + if (stream.readChar() != '\n') + { + throw new DataCorruptedException(" bad col ending"); + } + } + } + } + catch (IOException e) + { + LOGGER.error("Unable to write to file: ["+e.getMessage()+"].", e); + } + } + public void writeToFile(File file) + { + try(FileOutputStream fileStream = new FileOutputStream(file); + BufferedOutputStream bufferedStream = new BufferedOutputStream(fileStream); + DataOutputStream stream = new DataOutputStream(bufferedStream)) + { + for (int x = 0; x < LodUtil.CHUNK_WIDTH; x++) + { + for (int z = 0; z < LodUtil.CHUNK_WIDTH; z++) + { + for (int y = this.getMinBuildHeight(); y < this.getMaxBuildHeight(); y++) + { + stream.writeInt(this.blockOpacityStorage.get(new DhBlockPos(x, y, z).hashCode())); + stream.writeInt(this.blockEmissionStorage.get(new DhBlockPos(x, y, z).hashCode())); + } + + stream.writeChar(';'); + + stream.writeInt(this.lightBlockingHeightMap[x][z]); + stream.writeInt(this.solidHeightMap[x][z]); + + stream.writeChar('\n'); + } + } + + stream.flush(); + } + catch (IOException e) + { + LOGGER.error("Unable to write to file: ["+e.getMessage()+"].", e); + } + } + + /** + * Can be added into {@link com.seibel.distanthorizons.core.api.internal.SharedApi#applyChunkUpdate(IChunkWrapper, ILevelWrapper, boolean)} + * to save chunks to file for future testing. + */ + public void tryConvertingAndSavingChunkWrapper(IChunkWrapper chunkWrapper) + { + try + { + File chunkFile = new File(LightingEngineTest.TEST_DATA_PATH + "/" + chunkWrapper.getChunkPos().toString()); + if (!chunkFile.exists()) + { + LightingTestChunkWrapper testWrapper = new LightingTestChunkWrapper(chunkWrapper); + testWrapper.writeToFile(chunkFile); + } + } + catch (Exception e) + { + LOGGER.error(e.getMessage(), e); + } + } + + + + //===============// + // chunk methods // + //===============// + + @Override + public int getHeight() { return 255; } + + @Override + public int getMinBuildHeight() { return -64; } + @Override + public int getMaxBuildHeight() { return 255; } + + @Override + public int getMinNonEmptyHeight() + { + if (this.minNonEmptyHeight != Integer.MIN_VALUE) + { + return this.minNonEmptyHeight; + } + + + // default if every section is empty or missing + this.minNonEmptyHeight = this.getMinBuildHeight(); + + // determine the lowest empty section (bottom up) + int maxYHeight = this.getMaxBuildHeight(); + for (int y = this.getMinBuildHeight(); y < maxYHeight; y++) + { + if (this.blockOpacityStorage.get(new DhBlockPos(0, y, 0).hashCode()) != 0) + { + // -16 to simulate having to populate the full chunk section + this.minNonEmptyHeight = Math.min(y - 16, maxYHeight); + break; + } + } + + return this.minNonEmptyHeight; + } + + + @Override + public int getMaxNonEmptyHeight() + { + if (this.maxNonEmptyHeight != Integer.MAX_VALUE) + { + return this.maxNonEmptyHeight; + } + + + // default if every section is empty or missing + this.maxNonEmptyHeight = this.getMaxBuildHeight(); + + // determine the highest empty section (top down) + int minYHeight = this.getMinBuildHeight(); + for (int y = this.getMaxBuildHeight(); y >= minYHeight; y--) + { + if (this.blockOpacityStorage.get(new DhBlockPos(0, y, 0).hashCode()) != 0) + { + // -16 to simulate having to populate the full chunk section + this.maxNonEmptyHeight = Math.max(y - 16, minYHeight); + break; + } + } + + return this.maxNonEmptyHeight; + } + + + @Override + public int getSolidHeightMapValue(int xRel, int zRel) { return this.solidHeightMap[xRel][zRel]; } + + @Override + public int getLightBlockingHeightMapValue(int xRel, int zRel) { return this.lightBlockingHeightMap[xRel][zRel]; } + + + + @Override + public IBiomeWrapper getBiome(int relX, int relY, int relZ) { throw new UnsupportedOperationException("Not implemented"); } + + @Override + public DhChunkPos getChunkPos() { return this.chunkPos; } + + @Override + public int getMaxBlockX() { return 0; } + @Override + public int getMaxBlockZ() { return 0; } + @Override + public int getMinBlockX() { return LodUtil.CHUNK_WIDTH; } + @Override + public int getMinBlockZ() { return LodUtil.CHUNK_WIDTH; } + + @Override + public void setIsDhLightCorrect(boolean isDhLightCorrect) { } + + @Override + public void setUseDhLighting(boolean useDhLighting) { this.useDhLighting = useDhLighting; } + + + + @Override + public boolean isLightCorrect() { return false; } + + + @Override + public int getDhBlockLight(int relX, int y, int relZ) + { + this.throwIndexOutOfBoundsIfRelativePosOutsideChunkBounds(relX, y, relZ); + return this.getBlockLightStorage().get(relX, y, relZ); + } + @Override + public void setDhBlockLight(int relX, int y, int relZ, int lightValue) + { + this.throwIndexOutOfBoundsIfRelativePosOutsideChunkBounds(relX, y, relZ); + this.getBlockLightStorage().set(relX, y, relZ, lightValue); + } + + private ChunkLightStorage getBlockLightStorage() + { + if (this.blockLightStorage == null) + { + this.blockLightStorage = new ChunkLightStorage( + // +/- 16 is to fix an issue with the test chunk where the storage isn't big enough, + // James probably just screwed up the min/max height slightly + this.getMinBuildHeight() - 16, this.getMaxBuildHeight() + 16, + // positions above and below the handled area should be unlit + LodUtil.MIN_MC_LIGHT, LodUtil.MIN_MC_LIGHT); + } + return this.blockLightStorage; + } + + + @Override + public int getDhSkyLight(int relX, int y, int relZ) + { + this.throwIndexOutOfBoundsIfRelativePosOutsideChunkBounds(relX, y, relZ); + return this.getSkyLightStorage().get(relX, y, relZ); + } + @Override + public void setDhSkyLight(int relX, int y, int relZ, int lightValue) + { + this.throwIndexOutOfBoundsIfRelativePosOutsideChunkBounds(relX, y, relZ); + this.getSkyLightStorage().set(relX, y, relZ, lightValue); + } + + private ChunkLightStorage getSkyLightStorage() + { + if (this.skyLightStorage == null) + { + this.skyLightStorage = new ChunkLightStorage( + // +/- 16 is to fix an issue with the test chunk where the storage isn't big enough, + // James probably just screwed up the min/max height slightly + this.getMinBuildHeight() - 16, this.getMaxBuildHeight() + 16, + // positions above should be lit but positions below should be unlit + LodUtil.MAX_MC_LIGHT, LodUtil.MIN_MC_LIGHT); + } + return this.skyLightStorage; + } + + + @Override + public int getBlockLight(int relX, int y, int relZ) + { + this.throwIndexOutOfBoundsIfRelativePosOutsideChunkBounds(relX, y, relZ); + return this.getBlockLightStorage().get(relX, y, relZ); + } + + @Override + public int getSkyLight(int relX, int y, int relZ) + { + this.throwIndexOutOfBoundsIfRelativePosOutsideChunkBounds(relX, y, relZ); + return this.getSkyLightStorage().get(relX, y, relZ); + } + + @Override + public ArrayList getBlockLightPosList() { return this.blockLightPosList; } + + @Override + public boolean doNearbyChunksExist() { return false; } + + @Override + public String toString() { return this.chunkPos.toString(); } + + @Override + public IBlockStateWrapper getBlockState(int relX, int relY, int relZ) + { + this.throwIndexOutOfBoundsIfRelativePosOutsideChunkBounds(relX, relY, relZ); + + int opacity = this.blockOpacityStorage.get(new DhBlockPos(relX, relY, relZ).hashCode()); + int lightEmission = this.blockEmissionStorage.get(new DhBlockPos(relX, relY, relZ).hashCode()); + return new LightingTestBlockStateWrapper(opacity, lightEmission); + } + + @Override + public boolean isStillValid() { return true; } + + + +} diff --git a/core/src/test/java/tests/LightingEngineTest.java b/core/src/test/java/tests/LightingEngineTest.java new file mode 100644 index 000000000..cca1f5ff9 --- /dev/null +++ b/core/src/test/java/tests/LightingEngineTest.java @@ -0,0 +1,96 @@ +/* + * 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 tests; + +import com.seibel.distanthorizons.core.generation.DhLightingEngine; +import com.seibel.distanthorizons.core.pos.DhChunkPos; +import com.seibel.distanthorizons.core.util.LodUtil; +import com.seibel.distanthorizons.core.util.objects.DataCorruptedException; +import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; +import testItems.lightingEngine.LightingTestChunkWrapper; + +import java.io.File; +import java.util.ArrayList; + +/** + * Can be used to A/B Test lighting engine performance changes.

+ * + * normal - chunks: [1595], total Time: [1490], avg time: [0.9341692789968652]
+ * only surface light prop - chunks: [1595], total Time: [984], avg time: [0.6169278996865204]
+ * + * @author James Seibel + * @version 2024-6-11 + */ +public class LightingEngineTest +{ + /** + * There should be test data in the following core repo folder:
+ * Core\_Misc Files\test files\Lighting engine test chunk data.7z + */ + public static final String TEST_DATA_PATH = "C:/Users/James_Seibel/Desktop/test chunk data"; + + + //@Test + public void TestLightingEngine() throws DataCorruptedException + { + long totalNanoTime = 0; + int chunkCount = 0; + + File testFolder = new File(TEST_DATA_PATH); + File[] chunkFiles = testFolder.listFiles(); + for (int i = 0; i < chunkFiles.length; i++) + { + // chunk file parsing // + + File chunkFile = chunkFiles[i]; + + String fileName = chunkFile.getName(); // C[0,-3] + fileName = fileName.replace("C", "").replace("[", "").replace("]", ""); + int xPos = Integer.parseInt(fileName.split(",")[0]); + int zPos = Integer.parseInt(fileName.split(",")[1]); + DhChunkPos pos = new DhChunkPos(xPos, zPos); + + if (i % 100 == 0) + { + System.out.println(i + "/" + chunkFiles.length); + } + + LightingTestChunkWrapper chunk = new LightingTestChunkWrapper(pos, chunkFile); + chunkCount++; + + ArrayList nearbyChunkList = new ArrayList<>(1); + nearbyChunkList.add(chunk); + + + + // lighting // + + long startTime = System.nanoTime(); + DhLightingEngine.INSTANCE.lightChunk(chunk, nearbyChunkList, LodUtil.MAX_MC_LIGHT); + long endTime = System.nanoTime(); + totalNanoTime += endTime - startTime; + } + long timeMs = totalNanoTime / 1_000_000; + + + System.out.println("chunks: ["+chunkCount+"], total Time: ["+timeMs+"], avg time: ["+(timeMs/(double)chunkCount)+"]"); + } + +}