From 0f044531340fb348a45d2ada82d384895e0eec0a Mon Sep 17 00:00:00 2001 From: James Seibel Date: Sun, 3 Mar 2024 19:17:24 -0600 Subject: [PATCH] Make down sampling average values instead of grabbing the value closest to -inf --- .../fullData/sources/NewFullDataSource.java | 288 ++++++++++++++++-- .../transformers/LodDataBuilder.java | 10 + .../fullDatafile/NewFullDataFileHandler.java | 7 +- 3 files changed, 270 insertions(+), 35 deletions(-) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/NewFullDataSource.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/NewFullDataSource.java index b6b9c2deb..69c8f17fb 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/NewFullDataSource.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/fullData/sources/NewFullDataSource.java @@ -30,6 +30,7 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.util.FullDataPointUtil; import com.seibel.distanthorizons.core.util.LodUtil; +import com.seibel.distanthorizons.core.util.RenderDataPointUtil; import com.seibel.distanthorizons.core.util.objects.dataStreams.DhDataOutputStream; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.coreapi.ModInfo; @@ -41,10 +42,10 @@ import java.util.Arrays; import java.util.concurrent.locks.ReentrantLock; /** - * This data source contains every datapoint over its given {@link DhSectionPos}. - * + * This data source contains every datapoint over its given {@link DhSectionPos}.

+ * * TODO create a child object that extends AutoClosable - * that can be pooled so we don't have GC overhead + * that can be pooled to reduce GC overhead * * @see FullDataPointUtil * @see CompleteFullDataSource @@ -52,6 +53,8 @@ import java.util.concurrent.locks.ReentrantLock; public class NewFullDataSource implements IDataSource { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + /** useful for debugging, but can slow down update operations quite a bit due to being called so often. */ + private static final boolean RUN_UPDATE_DEV_VALIDATION = false; //ModInfo.IS_DEV_BUILD; /** measured in data columns */ public static final int WIDTH = 64; @@ -76,7 +79,10 @@ public class NewFullDataSource implements IDataSource */ public byte[] columnGenerationSteps; - /** stored x/z, y */ + /** + * stored x/z, y + * The y data should be sorted from bottom to top + */ public long[][] dataPoints; private boolean isEmpty; @@ -154,6 +160,15 @@ public class NewFullDataSource implements IDataSource public boolean update(NewFullDataSource inputDataSource, @Nullable IDhLevel level) { return this.update(inputDataSource); } public boolean update(NewFullDataSource inputDataSource) { + // shouldn't happen, but James saw it happen once + if (inputDataSource.mapping.getMaxValidId() == 0) + { + LOGGER.warn("Invalid mapping given from input update data source at pos: ["+inputDataSource.pos+"], skipping update."); + return false; + } + + + byte thisDetailLevel = this.pos.getDetailLevel(); byte inputDetailLevel = inputDataSource.pos.getDetailLevel(); @@ -263,38 +278,184 @@ public class NewFullDataSource implements IDataSource { for (int z = 0; z < WIDTH; z += 2) { - int inputIndex = relativePosToIndex(x, z); + long[] mergedInputDataArray = mergeInputTwoByTwoDataColumn(inputDataSource, x, z); + byte inputGenStep = inputDataSource.columnGenerationSteps[0]; // TODO - long[] inputDataArray = inputDataSource.dataPoints[inputIndex]; - if (inputDataArray != null) + + int recipientX = (x / 2) + recipientOffsetX; + int recipientZ = (z / 2) + recipientOffsetZ; + int recipientIndex = relativePosToIndex(recipientX, recipientZ); + + long[] oldDataArray = this.dataPoints[recipientIndex]; + + this.columnGenerationSteps[recipientIndex] = inputGenStep; + this.dataPoints[recipientIndex] = mergedInputDataArray; + this.remapDataColumn(recipientIndex, remappedIds); + + // we only need to see if the data was changed in one column + if (!dataChanged) { - byte inputGenStep = inputDataSource.columnGenerationSteps[inputIndex]; - - // TODO downsample instad of grabbing the column nearest to (-inf, -inf) - int recipientX = (x / 2) + recipientOffsetX; - int recipientZ = (z / 2) + recipientOffsetZ; - int recipientIndex = relativePosToIndex(recipientX, recipientZ); - - long[] oldDataArray = this.dataPoints[recipientIndex]; - - this.columnGenerationSteps[recipientIndex] = inputGenStep; - this.dataPoints[recipientIndex] = inputDataArray; - this.remapDataColumn(recipientIndex, remappedIds); - - // we only need to see if the data was changed in one column - if (!dataChanged) - { - // needs to be done after the ID's have been remapped otherwise the ID's won't match even if the data is the same - dataChanged = areDataColumnsDifferent(oldDataArray, this.dataPoints[recipientIndex]); - } - - this.isEmpty = false; + // needs to be done after the ID's have been remapped otherwise the ID's won't match even if the data is the same + dataChanged = areDataColumnsDifferent(oldDataArray, this.dataPoints[recipientIndex]); } + + this.isEmpty = false; } } return dataChanged; } + + + private static long[] mergeInputTwoByTwoDataColumn(NewFullDataSource inputDataSource, int x, int z) + { + ArrayList newColumnList = new ArrayList<>(); + + int[] currentDatapointIndex = new int[] { 0, 0, 0, 0 }; + + int lastId = 0; + byte lastBlockLight = 0; + byte lastSkyLight = 0; + int height = 0; + int minY = 0; + + + for (int blockY = 0; blockY < RenderDataPointUtil.MAX_WORLD_Y_SIZE; blockY++, height++) + { + //if (x == 38 && z == 2 && blockY == 65+72) // nether portal + if (x == 32 && z == 0 && blockY == 138) // glowstone + { + int k = 0; // TODO remove, just for testing + } + + // if each column has reached the end of their data, nothing more needs to be done + if (currentDatapointIndex[0] == -1 + && currentDatapointIndex[1] == -1 + && currentDatapointIndex[2] == -1 + && currentDatapointIndex[3] == -1 + ) + { + break; + } + + + long[] datapointsForYSlice = new long[4]; + + + // scary double loop but, + // this will only ever loop 4 times, + // once for each of the 4 input columns + int colIndex = 0; + for (int inputX = x; inputX < x +2; inputX++) + { + for (int inputZ = z; inputZ < z + 2; inputZ++, colIndex++) + { + long[] inputDataArray = inputDataSource.dataPoints[relativePosToIndex(inputX, inputZ)]; + if (inputDataArray == null || inputDataArray.length == 0) + { + currentDatapointIndex[colIndex] = -1; + continue; + } + + + int dataPointIndex = currentDatapointIndex[colIndex]; + if (dataPointIndex == -1) + { + // went over the end + continue; + } + long datapoint = inputDataArray[dataPointIndex]; + + int datapointMinY = FullDataPointUtil.getBottomY(datapoint); + int numbOfBlocksTall = FullDataPointUtil.getHeight(datapoint); + int datapointMaxY = (datapointMinY + numbOfBlocksTall); + + + // check if y position is inside this datapoint + if (blockY < datapointMinY) + { + // this y-slice is below this datapoint, nothing can be added + continue; + } + else if (blockY >= datapointMaxY) + { + // this y-slice is above this datapoint, + // try the next data point + + int newDatapointIndex = currentDatapointIndex[colIndex] + 1; + if (newDatapointIndex >= inputDataArray.length) + { + // went to far, no additional data present + newDatapointIndex = -1; + } + currentDatapointIndex[colIndex] = newDatapointIndex; + + + // try again with the next data point + inputZ--; + colIndex--; + continue; + } + + + + datapointsForYSlice[colIndex] = datapoint; + } + } + + + + int[] mergeIds = new int[4]; + int[] mergeBlockLights = new int[4]; + int[] mergeSkyLights = new int[4]; + for (int i = 0; i < 4; i++) + { + mergeIds[i] = FullDataPointUtil.getId(datapointsForYSlice[i]); + mergeBlockLights[i] = FullDataPointUtil.getBlockLight(datapointsForYSlice[i]); + mergeSkyLights[i] = FullDataPointUtil.getSkyLight(datapointsForYSlice[i]); + } + + + // determine the most common values for this slice + int id = determineMostValueInColumnSlice(mergeIds, inputDataSource.mapping); + byte blockLight = (byte) determineAverageValueInColumnSlice(mergeBlockLights); + byte skyLight = (byte) determineAverageValueInColumnSlice(mergeSkyLights); + + // if this slice is different then the last one, create a new one + if (id != lastId + // block and sky light might not be necessary + || blockLight != lastBlockLight + || skyLight != lastSkyLight) + { + if (height != 0) + { + newColumnList.add(FullDataPointUtil.encode(lastId, height, minY, lastBlockLight, lastSkyLight)); + } + + lastId = id; + lastBlockLight = blockLight; + lastSkyLight = skyLight; + height = 0; + minY = blockY; + } + } + + // add the last slice if present + if (height != 0) + { + newColumnList.add(FullDataPointUtil.encode(lastId, height, minY, lastBlockLight, lastSkyLight)); + } + + + + + long[]mergedInputDataArray = new long[newColumnList.size()]; + for (int i = 0; i < mergedInputDataArray.length; i++) + { + mergedInputDataArray[i] = newColumnList.get(i); + } + return mergedInputDataArray; + } /** * Only update the ID once it's been added to this data source. * Updating the incoming data source will cause issues if it is applied @@ -323,6 +484,72 @@ public class NewFullDataSource implements IDataSource return (newArrayHash != oldArrayHash); } } + /** @param mapping can be included to ignore air ID's, otherwise all 4 values are treated equally */ + private static int determineMostValueInColumnSlice(int[] sliceArray, @Nullable FullDataPointIdMap mapping) + { + if (RUN_UPDATE_DEV_VALIDATION) + { + LodUtil.assertTrue(sliceArray.length == 4, "Column Slice should only contain 4 values."); + } + + int value0 = sliceArray[0]; + int count0 = 0; + int value1 = sliceArray[1]; + int count1 = 0; + int value2 = sliceArray[2]; + int count2 = 0; + int value3 = sliceArray[3]; + int count3 = 0; + + // count the occurrences of each value + for (int i = 0; i < 4; i++) + { + int value = sliceArray[i]; + if (mapping != null && mapping.getBlockStateWrapper(value).isAir()) + { + // always overwrite air to prevent holes in hollow structures + continue; + } + + if (value == value0) + count0++; + else if (value == value1) + count1++; + else if (value == value2) + count2++; + else + count3++; + } + + // return the most common occurance + int maxCount = Math.max(count0, Math.max(count1, Math.max(count2, count3))); + if (maxCount == count0) + // if the max count is 1 then we'll just go with the first column + return value0; + else if (maxCount == count1) + return value1; + else if (maxCount == count2) + return value2; + else + return value3; + } + private static int determineAverageValueInColumnSlice(int[] sliceArray) + { + if (RUN_UPDATE_DEV_VALIDATION) + { + LodUtil.assertTrue(sliceArray.length == 4, "Column Slice should only contain 4 values."); + } + + + int value = 0; + for (int i = 0; i < 4; i++) + { + value += sliceArray[i]; + } + + value /= 4; + return value; + } @@ -377,10 +604,9 @@ public class NewFullDataSource implements IDataSource this.columnGenerationSteps[index] = worldGenStep.value; - // validate the incoming ID's - // shouldn't normally happen and can be disabled for release builds - if (ModInfo.IS_DEV_BUILD) + if (RUN_UPDATE_DEV_VALIDATION) { + // validate the incoming ID's int maxValidId = this.mapping.getMaxValidId(); for (int i = 0; i < longArray.length; i++) { diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java index 7b560e3d5..26d34d8bb 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java @@ -188,6 +188,16 @@ public class LodDataBuilder } longs.add(FullDataPointUtil.encode(mappedId, lastY - y, y + 1 - chunkWrapper.getMinBuildHeight(), blockLight, skyLight)); + // reverse the array so index 0 is the lowest, + // this is necessary for later logic + // source: https://stackoverflow.com/questions/2137755/how-do-i-reverse-an-int-array-in-java + for(int i = 0; i < longs.size() / 2; i++) + { + long temp = longs.getLong(i); + longs.set(i, longs.getLong(longs.size() - i - 1)); + longs.set(longs.size() - i - 1, temp); + } + dataSource.setSingleColumn(longs.toLongArray(), chunkX + chunkOffsetX, chunkZ + chunkOffsetZ, diff --git a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/NewFullDataFileHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/NewFullDataFileHandler.java index 1fe3796d3..59a3ccc3b 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/NewFullDataFileHandler.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/file/fullDatafile/NewFullDataFileHandler.java @@ -53,15 +53,14 @@ public class NewFullDataFileHandler { private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - private static final int NUMBER_OF_PARENT_UPDATE_TASKS_PER_THREAD = 20; + private static final int NUMBER_OF_PARENT_UPDATE_TASKS_PER_THREAD = 50; /** how many parent update tasks can be in the queue at once */ private static final int MAX_PARENT_UPDATE_TASK_COUNT = NUMBER_OF_PARENT_UPDATE_TASKS_PER_THREAD * Config.Client.Advanced.MultiThreading.numberOfFileHandlerThreads.get(); /** indicates how long the update queue thread should wait between queuing ticks */ - private static final int UPDATE_QUEUE_THREAD_DELAY_IN_MS = 1_000; + private static final int UPDATE_QUEUE_THREAD_DELAY_IN_MS = 500; - - // TODO add a debug view + /** the list of queued positions that need to update their parents */ Set parentApplicationPositionSet = ConcurrentHashMap.newKeySet(); private final ThreadPoolExecutor updateQueueProcessor = ThreadUtil.makeSingleThreadPool("Update Queue Processor"); private final AtomicBoolean updateQueueThreadRunningRef = new AtomicBoolean(false);