Merge branch 'distant-horizons-core-datapoint-reducer'

This commit is contained in:
James Seibel
2023-12-04 07:19:56 -06:00
2 changed files with 872 additions and 1 deletions
@@ -0,0 +1,859 @@
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;
if (ASSERTS) this.checkLinks();
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++;
}
}
//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.
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.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");
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 {
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() {
if (this.getSizeWithAir() <= 1) return;
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) {
this.setMinY(higher, this.getMinY(lowest));
this.resortSize(higher);
this.remove(lowest);
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));
}
}
//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);
}
}
//////////////////////////////// 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();
}
}
@@ -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<int[]> tLocalIndices = new ThreadLocal<>();
private static final ThreadLocal<boolean[]> tLocalIncreaseIndex = new ThreadLocal<>();
private static final ThreadLocal<boolean[]> tLocalIndexHandled = new ThreadLocal<>();
private static final ThreadLocal<short[]> tLocalHeightAndDepth = new ThreadLocal<>();
private static final ThreadLocal<int[]> 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
}
}
*/
}
}