From 42135636af60165581c79e71a67fbb7e7c011e59 Mon Sep 17 00:00:00 2001 From: Builderb0y Date: Sat, 25 Nov 2023 05:05:55 +0000 Subject: [PATCH 1/3] add and make use of RenderDataPointReducingList. --- .../util/RenderDataPointReducingList.java | 844 ++++++++++++++++++ .../core/util/RenderDataPointUtil.java | 14 +- 2 files changed, 857 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointReducingList.java diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointReducingList.java b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointReducingList.java new file mode 100644 index 000000000..8944ce7e2 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointReducingList.java @@ -0,0 +1,844 @@ +package com.seibel.distanthorizons.core.util; + +import com.google.common.annotations.VisibleForTesting; +import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnArrayView; +import com.seibel.distanthorizons.core.dataObjects.render.columnViews.IColumnDataView; +import com.seibel.distanthorizons.core.util.LodUtil.AssertFailureException; +import it.unimi.dsi.fastutil.longs.LongArrays; +import it.unimi.dsi.fastutil.shorts.ShortArrays; + +/** +a list of data points whose sole purpose is to {@link #reduce(int)} them. +each data point, henceforth referred to as a "node", is represented by 2 packed longs. +the "data" long contains the data point itself, as encoded by +{@link RenderDataPointUtil#createDataPoint(int, int, int, int, int, int, int, int, int)}. +the "links" long contains 4 packed 16-bit integers, which "point" to other nodes +in the sense that the index represented by the integer is another node in this list. +the 4 links are: bigger, smaller, higher, and lower. +all nodes are stored in 2 parallel long[]'s, namely {@link #data} and {@link #links}. + +all nodes are internally sorted in 2 different orders at the same time: +lowest-to-highest, and smallest-to-biggest. +both of these orders are important for reduction logic. +traversal in both orders is equally possible and important. + +@author Builderb0y +*/ +public class RenderDataPointReducingList { + + /** + setting this to true will cause the list to sanity-check + its own links automatically every time it modifies itself. + this is mostly just useful for debugging. + this should be set to false in production, + because these sanity checks are slow and happen often. + */ + private static final boolean ASSERTS = false; + /** + number of special cases to use for step 1 of {@link #reduce(int)}. + 2 works well for big globe worlds. + 0 is probably better for vanilla, but vanilla has vastly fewer segments/nodes + than big globe does, so the difference in efficiency matters a lot less. + */ + private static final int SPECIAL_CASES = 2; + public static final int + /** the bit offset of {@link #links} where the lower link is stored. */ + LOWER_SHIFT = 0, + /** the bit offset of {@link #links} where the higher link is stored. */ + HIGHER_SHIFT = 16, + /** the bit offset of {@link #links} where the smaller link is stored. */ + SMALLER_SHIFT = 32, + /** the bit offset of {@link #links} where the bigger link is stored. */ + BIGGER_SHIFT = 48, + /** + a bit mask for extracting links from elements of {@link #links}. + all links are 16 bits in length, so this constant has the lower 16 bits set, + and all remaining bits cleared. + */ + LINK_MASK = 0xFFFF, + /** a constant to indicate that a link is non-existent. */ + NULL = LINK_MASK; + public static final long + /** the default element of {@link #data} to indicate that there is no data. */ + DEFAUlT_DATA = 0L, + /** the default element of {@link #links} to indicate that a node is not linked to any other nodes. */ + DEFAULT_LINKS = -1L; + + /** + indexes of the nodes at the ends of this list. + access these fields through the getters, + not by the backing fields. the getters will + perform automatic short <-> int conversions. + + @implNote these fields behave as if they were unsigned, + and the getters will behave accordingly. + not that DH supports a wide enough Y range + to overflow these fields, but still. + */ + private short lowest, highest, smallest, biggest; + private short sizeWithAir, sizeWithoutAir; + private final long[] links, data; + /** + a temporary array to be used for sorting nodes. + the array is first populated such that every index + up to our current size represents a valid index. + then this array is sorted. + finally, the nodes are re-linked according + to the order of elements in this array. + */ + private final short[] sortingArray; + + public RenderDataPointReducingList(IColumnDataView view) { + int size = view.size(); + if (size == 0) { + this.setLowest(NULL); + this.setHighest(NULL); + this.setSmallest(NULL); + this.setBiggest(NULL); + this.links = LongArrays.EMPTY_ARRAY; + this.data = LongArrays.EMPTY_ARRAY; + this.sortingArray = ShortArrays.EMPTY_ARRAY; + return; + } + //allocate an array big enough to hold 2 * size - 1 nodes. + //this is the number of nodes we would have if none + //of the nodes in the provided view are touching, + //and we need to add air nodes between all of them. + //we will use this array for sorting the nodes, + //first by lowest-to-highest, then by smallest-to-biggest. + int arrayCapacity = (size << 1) - 1; + this.sortingArray = new short[arrayCapacity]; + this.links = new long[arrayCapacity]; + java.util.Arrays.fill(this.links, DEFAULT_LINKS); + this.data = new long[arrayCapacity]; + int sizeWithoutAir = 0; + for (int index = 0; index < size; index++) { + long packedData = view.get(index); + //first "pass" (if you can call it that) skips nodes with 0 height, and nodes that aren't visible. + //air nodes will be inserted *after* the nodes have been sorted by Y level. + if (isDataVisible(packedData) && RenderDataPointUtil.getYMin(packedData) < RenderDataPointUtil.getYMax(packedData)) { + this.setData(sizeWithoutAir, packedData); + this.setSortingIndex(sizeWithoutAir, sizeWithoutAir); + sizeWithoutAir++; + } + } + //sort the nodes by Y level. + this.sortByPosition(sizeWithoutAir); + //next pass: link the nodes together, and insert air nodes as necessary. + int sizeWithAir = sizeWithoutAir; + for (int sortingIndex = 1; sortingIndex < sizeWithoutAir; sortingIndex++) { + int lowerIndex = this.getSortingIndex(sortingIndex - 1); + int higherIndex = this.getSortingIndex(sortingIndex); + long lowerData = this.getData(lowerIndex); + long higherData = this.getData(higherIndex); + int lowerMaxY = RenderDataPointUtil.getYMax(lowerData); + int higherMinY = RenderDataPointUtil.getYMin(higherData); + if (lowerMaxY == higherMinY) { //the two nodes touch. + this.setHigher(lowerIndex, higherIndex); + this.setLower(higherIndex, lowerIndex); + } + else if (lowerMaxY < higherMinY) { //the two nodes do not touch. + this.setData( + sizeWithAir, + RenderDataPointUtil.createDataPoint( + 0, + 0, + 0, + 0, + higherMinY, + lowerMaxY, + RenderDataPointUtil.getLightSky(higherData), + RenderDataPointUtil.getLightBlock(higherData), + RenderDataPointUtil.getGenerationMode(higherData) + ) + ); + this.setSortingIndex(sizeWithAir, sizeWithAir); + this.setLower(higherIndex, sizeWithAir); + this.setHigher(lowerIndex, sizeWithAir); + this.setLower(sizeWithAir, lowerIndex); + this.setHigher(sizeWithAir, higherIndex); + sizeWithAir++; + } + else { //the two nodes overlap. + throw new IllegalArgumentException(RenderDataPointUtil.toString(lowerData) + " overlaps with " + RenderDataPointUtil.toString(higherData)); + } + } + this.lowest = this.sortingArray[0]; + this.highest = this.sortingArray[sizeWithoutAir - 1]; + + //now sort by size. + this.sortBySize(sizeWithAir); + for (int sortingIndex = 1; sortingIndex < sizeWithAir; sortingIndex++) { + int smallerIndex = this.getSortingIndex(sortingIndex - 1); + int biggerIndex = this.getSortingIndex(sortingIndex); + this.setBigger(smallerIndex, biggerIndex); + this.setSmaller(biggerIndex, smallerIndex); + } + this.smallest = this.sortingArray[0]; + this.biggest = this.sortingArray[sizeWithAir - 1]; + + this.setSizeWithAir(sizeWithAir); + this.setSizeWithoutAir(sizeWithoutAir); + + if (ASSERTS) this.checkLinks(); + } + + //////////////////////////////// operations //////////////////////////////// + + /** + verifies that this list is in the "correct" state, + and throws an {@link AssertFailureException} if it isn't. + */ + @VisibleForTesting + public void checkLinks() { + LodUtil.assertTrue(this.getSizeWithoutAir() <= this.getSizeWithAir(), "more segments without air than with air"); + if (this.getSizeWithAir() == 0) { + LodUtil.assertTrue(this.getSmallest() == NULL, "size is 0, but we have a smallest node"); + LodUtil.assertTrue(this.getBiggest() == NULL, "size is 0, but we have a biggest node"); + LodUtil.assertTrue(this.getLowest() == NULL, "size is 0, but we have a lowest node"); + LodUtil.assertTrue(this.getHighest() == NULL, "size is 0, but we have a highest node"); + } + else { + LodUtil.assertTrue(this.getSizeWithAir() > 0 && this.getSizeWithoutAir() >= 0, "at least one of our sizes is negative"); + int sizeWithAir = 0, sizeWithoutAir = 0; + for (int index = this.getSmallest(); index != NULL; index = this.getBigger(index)) { + int smaller = this.getSmaller(index); + int bigger = this.getBigger(index); + LodUtil.assertTrue((smaller != NULL ? this.getBigger(smaller) : this.getSmallest()) == index, "one-way link"); + LodUtil.assertTrue((bigger != NULL ? this.getSmaller(bigger) : this.getBiggest()) == index, "one-way link"); + LodUtil.assertTrue(smaller == NULL || this.getSize(index) >= this.getSize(smaller), "node is not sorted by size"); + sizeWithAir++; + if (this.isIndexVisible(index)) sizeWithoutAir++; + } + LodUtil.assertTrue(sizeWithAir == this.getSizeWithAir() && sizeWithoutAir == this.getSizeWithoutAir(), "node count does not match size"); + + sizeWithAir = sizeWithoutAir = 0; + for (int index = this.getLowest(); index != NULL; index = this.getHigher(index)) { + int lower = this.getLower(index); + int higher = this.getHigher(index); + LodUtil.assertTrue((lower != NULL ? this.getHigher(lower) : this.getLowest()) == index, "one-way link"); + LodUtil.assertTrue((higher != NULL ? this.getLower(higher) : this.getHighest()) == index, "one-way link"); + LodUtil.assertTrue(this.getMaxY(index) > this.getMinY(index), "node has inverted Y levels"); + LodUtil.assertTrue(lower == NULL || this.getMinY(index) == this.getMaxY(lower), "node does not touch its lower neighbor"); + sizeWithAir++; + if (this.isIndexVisible(index)) sizeWithoutAir++; + } + LodUtil.assertTrue(sizeWithAir == this.getSizeWithAir() && sizeWithoutAir == this.getSizeWithoutAir(), "node count does not match size"); + } + } + + /** removes the node at the given index from this list. */ + public void remove(int index) { + int + lower = this.getLower (index), + higher = this.getHigher (index), + smaller= this.getSmaller(index), + bigger = this.getBigger (index), + alpha = this.getAlpha (index); + if (lower != NULL) this.setHigher(lower, higher); + else this.setLowest(higher); + if (higher != NULL) this.setLower(higher, lower); + else this.setHighest(lower); + if (smaller != NULL) this.setBigger(smaller, bigger); + else this.setSmallest(bigger); + if (bigger != NULL) this.setSmaller(bigger, smaller); + else this.setBiggest(smaller); + this.setData(index, DEFAUlT_DATA); + this.links[index] = DEFAULT_LINKS; + this.sizeWithAir--; + if (isAlphaVisible(alpha)) this.sizeWithoutAir--; + } + + /** + refreshes the smallest-to-biggest order of this list. + as a reminder, the list is internally sorted from smallest-to-biggest + and lowest-to-highest at the same time. part of reduction logic + can invalidate the smallest-to-biggest order, so this method re-computes it. + this method does not touch the lowest-to-highest order of the list. + + this method requires that all nodes are already sorted from + lowest-to-highest, so it is not applicable to use this method in + the constructor before the lowest-to-highest order is initialized. + */ + @VisibleForTesting + public void sortBySizeAndReLink() { + long[] datas = this.data; + int writeIndex = 0; + for (int readIndex = this.getLowest(); readIndex != NULL; readIndex = this.getHigher(readIndex)) { + if (datas[readIndex] != DEFAUlT_DATA) { + this.setSortingIndex(writeIndex++, readIndex); + } + } + this.sortBySize(writeIndex); + for (int index = 1; index < writeIndex; index++) { + int smaller = this.getSortingIndex(index - 1); + int bigger = this.getSortingIndex(index); + this.setSmaller(bigger, smaller); + this.setBigger(smaller, bigger); + } + this.smallest = this.sortingArray[0]; + this.biggest = this.sortingArray[writeIndex - 1]; + this.setSmaller(this.getSmallest(), NULL); + this.setBigger(this.getBiggest(), NULL); + } + + /** + sorts our {@link #sortingArray} in order of smallest-to-biggest, + but does NOT update our links accordingly. + */ + @VisibleForTesting + public void sortBySize(int size) { + short[] array = this.sortingArray; + it.unimi.dsi.fastutil.Arrays.quickSort( + 0, + size, + (int index1, int index2) -> { + return Integer.compare( + this.getSize(this.getSortingIndex(index1)), + this.getSize(this.getSortingIndex(index2)) + ); + }, + (int index1, int index2) -> { + ShortArrays.swap(array, index1, index2); + } + ); + } + + /** + sorts our {@link #sortingArray} in order of lowest-to-highest, + but does NOT update our links accordingly. + */ + @VisibleForTesting + public void sortByPosition(int size) { + short[] array = this.sortingArray; + it.unimi.dsi.fastutil.Arrays.quickSort( + 0, + size, + (int index1, int index2) -> { + return Integer.compare( + this.getMinY(this.getSortingIndex(index1)), + this.getMinY(this.getSortingIndex(index2)) + ); + }, + (int index1, int index2) -> { + ShortArrays.swap(array, index1, index2); + } + ); + } + + /** + moves the smaller node to the correct position in the list, + under the assumption that all other nodes are already sorted. + this method should be called when the smaller node is + merged with another node, causing it to become bigger. + + important: this method ONLY handles the case where a node + is made bigger. it does NOT handle the case where a node + is made smaller. if the node is made smaller, it will be + left in its current position, even if that position is wrong. + */ + public void resortSize(int smaller) { + int bigger = this.getBigger(smaller); + + //check if the node needs to be moved at all, + //and return if it doesn't. + if (bigger == NULL || this.getSize(smaller) <= this.getSize(bigger)) return; + + //first remove smaller from before bigger. + int smallest = this.getSmaller(smaller); + if (smallest != NULL) this.setBigger(smallest, bigger); + else this.setSmallest(bigger); + this.setSmaller(bigger, smallest); + + //next, find the position to re-insert the node. + do bigger = this.getBigger(bigger); + while (bigger != NULL && this.getSize(smaller) > this.getSize(bigger)); + + //lastly, re-insert the node where it belongs. + this.setSmaller(smaller, bigger != NULL ? this.getSmaller(bigger) : this.getBiggest()); + this.setBigger(smaller, bigger); + if (bigger != NULL) this.setSmaller(bigger, smaller); + else this.setBiggest(smaller); + smallest = this.getSmaller(smaller); + if (smallest != NULL) this.setBigger(smallest, smaller); + else this.setSmallest(smaller); + } + + /** + shared logic for merging segments in step 1 documented in {@link #reduce(int)}. + + returns the index of the next node to be used for iteration. + + @param fastPath if true, we are in the "fast path" for removing + segments whose size is less than or equal to {@link #SPECIAL_CASES}. + this fast path functions somewhat differently from the normal path, + the important things to note for this method are: + + the fast path does not re-sort nodes when their size changes. + this leaves the list in an invalid state, and it is up to the caller to re-sort + the list via {@link #sortBySizeAndReLink()} after the fast path is done. + + at the time of writing this, the fast path iterates in reverse order. + as such, when fastPath is set to true, this method will return + current's smaller neighbor, when fastPath is set to false, + this method will return current's bigger neighbor instead. + */ + private int tryMergeStep1(int current, boolean fastPath) { + int + result = fastPath ? this.getSmaller(current) : this.getBigger(current), + higher = this.getHigher(current), + lower = this.getLower(current), + toExtendDownwards, + toRemove; + if (higher != NULL && this.getAlpha(higher) == this.getAlpha(current)) { + if (lower != NULL && this.getAlpha(lower) == this.getAlpha(current)) { + if (this.getSize(higher) <= this.getSize(lower)) { + toExtendDownwards = higher; + toRemove = current; + } + else { + toExtendDownwards = current; + toRemove = lower; + } + } + else { + toExtendDownwards = higher; + toRemove = current; + } + } + else { + if (lower != NULL && this.getAlpha(lower) == this.getAlpha(current)) { + toExtendDownwards = current; + toRemove = lower; + } + else { + return result; + } + } + //if we're about to remove the next node for iteration, + //then we need to continue iterating at the node after that. + //result will only be returned if fastPath is true, + //so the node after that is always the smaller one. that's why I don't need to do + //if (result == toRemove) result = fastPath ? this.getSmaller(result) : this.getBigger(result); + if (result == toRemove) result = this.getSmaller(result); + this.setMinY(toExtendDownwards, this.getMinY(toRemove)); + if (!fastPath) this.resortSize(toExtendDownwards); + this.remove(toRemove); + //if we're NOT on the fast path, and we reach this line, + //then we have just modified the list in a way which may + //invalidate assumptions made by the step 1 loop. + //so, return smallest to signal that the loop should start over. + //starting over is not usually a big deal, + //because small nodes are usually merged quite quickly. + //in my testing, I didn't see the step 1 loop run more + //than twice as many times as the starting list size. + return fastPath ? result : this.getSmallest(); + } + + /** + returns the largest node whose height is strictly less than the provided size, + or null if all contained nodes are greater than or equal to the provided size. + + special cases: + if the list is empty, then null is returned, + because the loop will not run and biggest will be null. + + if all nodes are less tall than size, then the largest node is returned, + because the loop will run for all nodes, but will not return any of them, + so the fallback path of returning the biggest node is used. + + if all nodes are at least as tall as size, then null is returned, + because the loop will immediately return the + smallest node's smaller neighbor, which is null. + */ + private int lowerNode(int size) { + for (int node = this.getSmallest(); node != NULL; node = this.getBigger(node)) { + if (this.getSize(node) >= size) return this.getSmaller(node); + } + return this.getBiggest(); + } + + /** + handles special cases for step 1 of {@link #reduce(int)}. + in other words, handles all the nodes whose size + is less than or equal to {@link #SPECIAL_CASES}. + + returns true if this step single-handedly brought + the list's size down to less than or equal to target, + or false if more steps need to be performed. + */ + private boolean reduceStep1SpecialCases(int target) { + for (int specialCase = 1; specialCase <= SPECIAL_CASES; specialCase++) { + for (int current = this.lowerNode(specialCase + 1); current != NULL;) { + if (this.getSizeWithoutAir() <= target) { + this.sortBySizeAndReLink(); + if (ASSERTS) this.checkLinks(); + return true; + } + current = this.tryMergeStep1(current, true); + } + this.sortBySizeAndReLink(); + if (ASSERTS) this.checkLinks(); + } + return false; + } + + /** + handles the general case for step 1 of {@link #reduce(int)}. + in other words, handles all the nodes whose size + is strictly greater than {@link #SPECIAL_CASES}, + and all the nodes which are smaller, but failed + to be merged in {@link #reduceStep1SpecialCases(int)} + + returns true if this step single-handedly brought + the list's size down to less than or equal to target, + or false if more steps need to be performed. + */ + private boolean reduceStep1GeneralCases(int target) { + for (int current = this.getSmallest(); current != NULL;) { + if (this.getSizeWithoutAir() <= target) return true; + current = this.tryMergeStep1(current, false); + if (ASSERTS) this.checkLinks(); + } + return false; + } + + /** + handles step 2 of {@link #reduce(int)}, where nodes are allowed to be erased. + + returns true if this step single-handedly brought + the list's size down to less than or equal to target, + or false if more steps need to be performed. + */ + private boolean reduceStep2(int target) { + for (int center = this.getSmallest(); center != NULL;) { + if (this.getSizeWithoutAir() <= target) return true; + int lower = this.getLower (center); + int higher = this.getHigher(center); + if (lower != NULL && higher != NULL && this.getAlpha(lower) == this.getAlpha(higher)) { + this.setMinY(higher, this.getMinY(lower)); + this.resortSize(higher); + this.remove(lower); + this.remove(center); + if (ASSERTS) this.checkLinks(); + center = this.getSmallest(); + } + else { + center = this.getBigger(center); + } + } + return false; + } + + /** + handles step 3 of {@link #reduce(int)}, where nodes + are forced to merge in order to fit the desired target, + even if they normally shouldn't merge because it would look bad. + + returns true if this step brought the list's + size down to less than or equal to target, + or false if we need to go back to step 1. + */ + private boolean reduceStep3(int target) { + if (this.getSizeWithoutAir() <= target) return true; + int lowest = this.getLowest(); + int higher = this.getHigher(lowest); + if (higher != NULL) { + if (this.getAlpha(higher) >= this.getAlpha(lowest)) { + this.setMinY(higher, this.getMinY(lowest)); + this.remove(lowest); + } + else { + this.setMaxY(lowest, this.getMaxY(higher)); + this.resortSize(lowest); + this.remove(higher); + } + if (ASSERTS) this.checkLinks(); + return false; //go back to step 1. + } + else { + //if we reach this line, then target is 0 or negative. + this.setLowest(NULL); + this.setHighest(NULL); + this.setSmallest(NULL); + this.setBiggest(NULL); + this.setSizeWithAir(0); + this.setSizeWithoutAir(0); + return true; + } + } + + /** + merges and/or eliminates nodes until our {@link #sizeWithoutAir} + is less than or equal to the provided target size. + this method assumes that the list is already sorted by size. + if it is not sorted, you should call {@link #sortBySizeAndReLink()} first. + note also that the list is sorted in its constructor, + so if this is a new, unmodified list, then it is safe to call this method. + + algorithm: + 1: try to merge the smallest segment with the segment above or below it. + this will only succeed if the adjacent node has the same alpha as it. + 1a: if there is only one adjacent node which matches this criteria, + we will merge with that node. + + 1b: if both adjacent nodes match this criteria, + attempt to merge with the smaller one. + 1b1: if both adjacent nodes are the same height, + merge with the higher one. + + 1c: if there are no adjacent nodes which match this criteria, + repeat step 1 with the next smallest segment instead. + continue trying bigger and bigger segments until we either: + * have a success, or + * reach the end of this list. + 2: if we reach the end of the list before having a success, try again, + but this time, we are allowed to erase a segment entirely without merging it + if there are equal-alpha'd segments above and below it. + 3: if we still fail, force the lowest segment to merge with the segment above it, + with no restrictions on alpha. + the highest alpha of the two segments takes priority though. + 4: repeat until our size is less than or equal to the target size. + notes: + changing the size of a node requires re-sorting that node, + but it does not require re-sorting the whole list. + additionally, because of the fact that nodes are sorted smallest to biggest, + when a node is expanded, its new size will be + strictly less than or equal to twice its old size. + the significance of this is that in practice, + nodes should not need to be moved very far to be re-sorted. + + special case: there are a lot of segments of length 1 in big globe worlds. + these will genuinely have a long way to move on re-sort. + so, they are handled in a separate loop. + + after step 1 is completed, step 2 can't change the + list in a way which would give step 1 more work to do, + so step 2 is repeated as many times as necessary, + without jumping back to the start. + step 3 however can change the list in a way which gives previous + steps more work to do, so after step 3 merges something, + we jump back to step 1 and start over. + */ + public void reduce(int target) { + if (this.reduceStep1SpecialCases(target)) return; + + while (true) { + if (this.reduceStep1GeneralCases(target)) return; + if (this.reduceStep2(target)) return; + if (this.reduceStep3(target)) return; + } + } + + /** transfers the contents of this list to the provided view, in order of highest to lowest. */ + public void copyTo(ColumnArrayView view) { + //reminder: DH explodes horribly when I copy the nodes + //from lowest to highest instead of highest to lowest. + int writeIndex = 0; + for (int node = this.getHighest(); node != NULL; node = this.getLower(node)) { + if (this.isIndexVisible(node)) { + view.set(writeIndex++, this.getData(node)); + } + } + for (int size = view.size(); writeIndex < size; writeIndex++) { + view.set(writeIndex, 0L); + } + } + + //////////////////////////////// getters //////////////////////////////// + + public int getSmallest() { + return Short.toUnsignedInt(this.smallest); + } + + public int getBiggest() { + return Short.toUnsignedInt(this.biggest); + } + + public int getLowest() { + return Short.toUnsignedInt(this.lowest); + } + + public int getHighest() { + return Short.toUnsignedInt(this.highest); + } + + public int getSizeWithAir() { + return Short.toUnsignedInt(this.sizeWithAir); + } + + public int getSizeWithoutAir() { + return Short.toUnsignedInt(this.sizeWithoutAir); + } + + public int getSortingIndex(int index) { + return Short.toUnsignedInt(this.sortingArray[index]); + } + + public int getLower(int index) { + return ((int)(this.links[index] >>> LOWER_SHIFT)) & LINK_MASK; + } + + public int getHigher(int index) { + return ((int)(this.links[index] >>> HIGHER_SHIFT)) & LINK_MASK; + } + + public int getSmaller(int index) { + return ((int)(this.links[index] >>> SMALLER_SHIFT)) & LINK_MASK; + } + + public int getBigger(int index) { + return ((int)(this.links[index] >>> BIGGER_SHIFT)) & LINK_MASK; + } + + public long getData(int index) { + return this.data[index]; + } + + public int getMinY(int index) { + return RenderDataPointUtil.getYMin(this.getData(index)); + } + + public int getMaxY(int index) { + return RenderDataPointUtil.getYMax(this.getData(index)); + } + + public int getSize(int index) { + long data = this.getData(index); + return RenderDataPointUtil.getYMax(data) - RenderDataPointUtil.getYMin(data); + } + + public int getRed(int index) { + return RenderDataPointUtil.getRed(this.getData(index)); + } + + public int getGreen(int index) { + return RenderDataPointUtil.getGreen(this.getData(index)); + } + + public int getBlue(int index) { + return RenderDataPointUtil.getBlue(this.getData(index)); + } + + public int getAlpha(int index) { + return RenderDataPointUtil.getAlpha(this.getData(index)); + } + + public int getBlockLight(int index) { + return RenderDataPointUtil.getLightBlock(this.getData(index)); + } + + public int getSkyLight(int index) { + return RenderDataPointUtil.getLightSky(this.getData(index)); + } + + //////////////////////////////// setters //////////////////////////////// + + public void setSmallest(int smallest) { + this.smallest = (short)(smallest); + } + + public void setBiggest(int biggest) { + this.biggest = (short)(biggest); + } + + public void setLowest(int lowest) { + this.lowest = (short)(lowest); + } + + public void setHighest(int highest) { + this.highest = (short)(highest); + } + + public void setSizeWithAir(int sizeWithAir) { + this.sizeWithAir = (short)(sizeWithAir); + } + + public void setSizeWithoutAir(int sizeWithoutAir) { + this.sizeWithoutAir = (short)(sizeWithoutAir); + } + + public void setSortingIndex(int index, int to) { + this.sortingArray[index] = (short)(to); + } + + public void setLower(int index, int lowerIndex) { + this.links[index] = (this.links[index] & ~(((long)(LINK_MASK)) << LOWER_SHIFT)) | (((long)(lowerIndex & LINK_MASK)) << LOWER_SHIFT); + } + + public void setHigher(int index, int higherIndex) { + this.links[index] = (this.links[index] & ~(((long)(LINK_MASK)) << HIGHER_SHIFT)) | (((long)(higherIndex & LINK_MASK)) << HIGHER_SHIFT); + } + + public void setSmaller(int index, int smallerIndex) { + this.links[index] = (this.links[index] & ~(((long)(LINK_MASK)) << SMALLER_SHIFT)) | (((long)(smallerIndex & LINK_MASK)) << SMALLER_SHIFT); + } + + public void setBigger(int index, int biggerIndex) { + this.links[index] = (this.links[index] & ~(((long)(LINK_MASK)) << BIGGER_SHIFT)) | (((long)(biggerIndex & LINK_MASK)) << BIGGER_SHIFT); + } + + public void setData(int index, long data) { + this.data[index] = data; + } + + public void setMinY(int index, int minY) { + this.data[index] = (this.data[index] & ~RenderDataPointUtil.DEPTH_SHIFTED_MASK) | ((minY & RenderDataPointUtil.DEPTH_MASK) << RenderDataPointUtil.DEPTH_SHIFT); + } + + public void setMaxY(int index, int maxY) { + this.data[index] = (this.data[index] & ~RenderDataPointUtil.HEIGHT_SHIFTED_MASK) | ((maxY & RenderDataPointUtil.HEIGHT_MASK) << RenderDataPointUtil.HEIGHT_SHIFT); + } + + public void setRed(int index, int red) { + this.data[index] = (this.data[index] & ~(RenderDataPointUtil.RED_MASK << RenderDataPointUtil.RED_SHIFT)) | ((red & RenderDataPointUtil.RED_MASK) << RenderDataPointUtil.RED_SHIFT); + } + + public void setGreen(int index, int green) { + this.data[index] = (this.data[index] & ~(RenderDataPointUtil.GREEN_MASK << RenderDataPointUtil.GREEN_SHIFT)) | ((green & RenderDataPointUtil.GREEN_MASK) << RenderDataPointUtil.GREEN_SHIFT); + } + + public void setBlue(int index, int blue) { + this.data[index] = (this.data[index] & ~(RenderDataPointUtil.BLUE_MASK << RenderDataPointUtil.BLUE_SHIFT)) | ((blue & RenderDataPointUtil.BLUE_MASK) << RenderDataPointUtil.BLUE_SHIFT); + } + + public void setAlpha(int index, int alpha) { + alpha >>>= RenderDataPointUtil.ALPHA_DOWNSIZE_SHIFT; + this.data[index] = (this.data[index] & ~(RenderDataPointUtil.ALPHA_MASK << RenderDataPointUtil.ALPHA_SHIFT)) | ((alpha & RenderDataPointUtil.ALPHA_MASK) << RenderDataPointUtil.ALPHA_SHIFT); + } + + public void setBlockLight(int index, int blockLight) { + this.data[index] = (this.data[index] & ~(RenderDataPointUtil.BLOCK_LIGHT_MASK << RenderDataPointUtil.BLOCK_LIGHT_SHIFT)) | ((blockLight & RenderDataPointUtil.BLOCK_LIGHT_MASK) << RenderDataPointUtil.BLOCK_LIGHT_SHIFT); + } + + public void setSkyLight(int index, int skyLight) { + this.data[index] = (this.data[index] & ~(RenderDataPointUtil.SKY_LIGHT_MASK << RenderDataPointUtil.SKY_LIGHT_SHIFT)) | ((skyLight & RenderDataPointUtil.SKY_LIGHT_MASK) << RenderDataPointUtil.SKY_LIGHT_SHIFT); + } + + //////////////////////////////// utility //////////////////////////////// + + public boolean isIndexVisible(int index) { + return isDataVisible(this.getData(index)); + } + + public static boolean isDataVisible(long data) { + return isAlphaVisible(RenderDataPointUtil.getAlpha(data)); + } + + public static boolean isAlphaVisible(int alpha) { + return alpha >= 16; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(this.sizeWithAir << 8).append("lowest to highest:"); + for (int index = this.lowest; index != NULL; index = this.getHigher(index)) { + builder.append('\n').append(RenderDataPointUtil.toString(this.getData(index))); + } + builder.append("\nsmallest to biggest:"); + for (int index = this.smallest; index != NULL; index = this.getBigger(index)) { + builder.append('\n').append(RenderDataPointUtil.toString(this.getData(index))); + } + return builder.toString(); + } +} \ No newline at end of file diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointUtil.java b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointUtil.java index 4edca2d40..3cee54b34 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointUtil.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointUtil.java @@ -256,12 +256,17 @@ public class RenderDataPointUtil // TODO this should probably be moved // TODO what is the purpose of these? + //these were needed by the old logic for mergeMultiData(), + //which has now been replaced by RenderDataPointReducingList. + //so, these are no longer necessary, but left here for the same + //reason the old logic is left here: in case it's ever needed again. + /* private static final ThreadLocal tLocalIndices = new ThreadLocal<>(); private static final ThreadLocal tLocalIncreaseIndex = new ThreadLocal<>(); private static final ThreadLocal tLocalIndexHandled = new ThreadLocal<>(); private static final ThreadLocal tLocalHeightAndDepth = new ThreadLocal<>(); private static final ThreadLocal tDataIndexCache = new ThreadLocal<>(); - + */ /** * This method merge column of multiple data together @@ -271,6 +276,12 @@ public class RenderDataPointUtil */ public static void mergeMultiData(IColumnDataView sourceData, ColumnArrayView output) { + RenderDataPointReducingList list = new RenderDataPointReducingList(sourceData); + list.reduce(output.verticalSize()); + list.copyTo(output); + + //old logic left here in case it's ever needed again. + /* if (output.dataCount() != 1) { throw new IllegalArgumentException("output must be only reserved for one datapoint!"); @@ -612,6 +623,7 @@ public class RenderDataPointUtil } } + */ } } \ No newline at end of file From d21244ce233cc62111303fb267bd926b800ec7e2 Mon Sep 17 00:00:00 2001 From: Builderb0y Date: Mon, 27 Nov 2023 01:52:22 +0000 Subject: [PATCH 2/3] handle edge case where there are many segments to merge but all of them are invisible. --- .../util/RenderDataPointReducingList.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointReducingList.java b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointReducingList.java index 8944ce7e2..25243147c 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointReducingList.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointReducingList.java @@ -98,6 +98,7 @@ public class RenderDataPointReducingList { this.links = LongArrays.EMPTY_ARRAY; this.data = LongArrays.EMPTY_ARRAY; this.sortingArray = ShortArrays.EMPTY_ARRAY; + if (ASSERTS) this.checkLinks(); return; } //allocate an array big enough to hold 2 * size - 1 nodes. @@ -122,6 +123,18 @@ public class RenderDataPointReducingList { sizeWithoutAir++; } } + + //check if all segments to merge are air or otherwise invisible (barriers). + //if they are, then this list can stay empty. + if (sizeWithoutAir == 0) { + this.setLowest(NULL); + this.setHighest(NULL); + this.setSmallest(NULL); + this.setBiggest(NULL); + if (ASSERTS) this.checkLinks(); + return; + } + //sort the nodes by Y level. this.sortByPosition(sizeWithoutAir); //next pass: link the nodes together, and insert air nodes as necessary. @@ -191,6 +204,8 @@ public class RenderDataPointReducingList { */ @VisibleForTesting public void checkLinks() { + LodUtil.assertTrue(this.getSizeWithAir() >= 0, "size with air < 0"); + LodUtil.assertTrue(this.getSizeWithoutAir() >= 0, "size without air < 0"); LodUtil.assertTrue(this.getSizeWithoutAir() <= this.getSizeWithAir(), "more segments without air than with air"); if (this.getSizeWithAir() == 0) { LodUtil.assertTrue(this.getSmallest() == NULL, "size is 0, but we have a smallest node"); @@ -199,7 +214,6 @@ public class RenderDataPointReducingList { LodUtil.assertTrue(this.getHighest() == NULL, "size is 0, but we have a highest node"); } else { - LodUtil.assertTrue(this.getSizeWithAir() > 0 && this.getSizeWithoutAir() >= 0, "at least one of our sizes is negative"); int sizeWithAir = 0, sizeWithoutAir = 0; for (int index = this.getSmallest(); index != NULL; index = this.getBigger(index)) { int smaller = this.getSmaller(index); @@ -262,6 +276,7 @@ public class RenderDataPointReducingList { */ @VisibleForTesting public void sortBySizeAndReLink() { + if (this.getSizeWithAir() <= 1) return; long[] datas = this.data; int writeIndex = 0; for (int readIndex = this.getLowest(); readIndex != NULL; readIndex = this.getHigher(readIndex)) { @@ -640,6 +655,12 @@ public class RenderDataPointReducingList { view.set(writeIndex++, this.getData(node)); } } + //this list could be empty if all the segments for merging are invisible, + //but we must ensure that the view is non-empty. + //so, if we didn't set any data points, add a void data point. + if (writeIndex == 0) { + view.set(writeIndex++, RenderDataPointUtil.createVoidDataPoint((byte)(1))); + } for (int size = view.size(); writeIndex < size; writeIndex++) { view.set(writeIndex, 0L); } From ca9dfed516ddcc336c2bb95d37dcecd2b381f9e7 Mon Sep 17 00:00:00 2001 From: Builderb0y Date: Fri, 1 Dec 2023 15:50:38 +0000 Subject: [PATCH 3/3] use higher Y level instead of higher opacity when forcing the lowest segment to merge. --- .../core/util/RenderDataPointReducingList.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointReducingList.java b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointReducingList.java index 25243147c..c19f6f149 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointReducingList.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/RenderDataPointReducingList.java @@ -559,15 +559,9 @@ public class RenderDataPointReducingList { int lowest = this.getLowest(); int higher = this.getHigher(lowest); if (higher != NULL) { - if (this.getAlpha(higher) >= this.getAlpha(lowest)) { - this.setMinY(higher, this.getMinY(lowest)); - this.remove(lowest); - } - else { - this.setMaxY(lowest, this.getMaxY(higher)); - this.resortSize(lowest); - this.remove(higher); - } + this.setMinY(higher, this.getMinY(lowest)); + this.resortSize(higher); + this.remove(lowest); if (ASSERTS) this.checkLinks(); return false; //go back to step 1. }