Improve node out-of-bound logic

This fixes some overlapping rendering issues, fixes LOD generating outside of render distance, and fixes low-detail LODs flashing when moving into previously-explored LODs
This commit is contained in:
James Seibel
2026-04-21 19:49:50 -05:00
parent 5dcda31990
commit 1debd4b875
4 changed files with 224 additions and 83 deletions
@@ -246,21 +246,37 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
//region
// remove out of bound sections
this.setCenterBlockPos(playerPos, (renderSection) ->
{
if (renderSection != null)
this.setCenterBlockPos(playerPos,
// remove completely out of bound nodes
// (the root node is no longer in bounds)
(renderSection) ->
{
this.fullDataSourceProvider.removeRetrievalRequestIf((long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
// unfortunately we have to fully go through each set
// since a removed position may be larger than the multiple generated positions
// it contains
this.missingGenerationPosSet.removeIf((Long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
this.queuedGenerationPosSet.removeIf((Long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
renderSection.close();
if (renderSection != null)
{
this.fullDataSourceProvider.removeRetrievalRequestIf((long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
// unfortunately we have to fully go through each set
// since a removed position may be larger than the multiple generated positions
// it contains
this.missingGenerationPosSet.removeIf((Long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
this.queuedGenerationPosSet.removeIf((Long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
renderSection.close();
}
},
// mutate partially out of bound nodes
// (the root node is still in bounds, but this individual child node isn't)
(renderSection) ->
{
if (renderSection != null)
{
// when this node comes back into render distance
// we'll need to re-load it since the full data
// may have been modified while it was out of bounds
renderSection.renderDataDirty = true;
}
}
});
);
//endregion
@@ -309,7 +325,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
continue;
}
node.value.retreivedMissingSectionsForRetreival = false;
node.value.queuedMissingSectionsForRetrieval = false;
}
}
@@ -452,9 +468,9 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
// since this section wants to render
// check if it needs any generation to do so
if (!node.value.retreivedMissingSectionsForRetreival)
if (!node.value.queuedMissingSectionsForRetrieval)
{
node.value.retreivedMissingSectionsForRetreival = true;
node.value.queuedMissingSectionsForRetrieval = true;
this.tryQueuePosForRetrieval(node.value.pos); // can be quite slow
}
}
@@ -481,27 +497,6 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
//=========================//
//region
@NotNull
private QuadNode<LodRenderSection> tryAddNodeToTree(
@NotNull QuadNode<LodRenderSection> rootNode,
@Nullable QuadNode<LodRenderSection> quadNode,
long sectionPos // section pos is needed here since the quad node may be null
)
{
// create the node
if (quadNode == null)
{
rootNode.setValue(sectionPos, new LodRenderSection(sectionPos, this, this.level, this.fullDataSourceProvider));
quadNode = rootNode.getNode(sectionPos);
}
if (quadNode == null)
{
LodUtil.assertNotReach("Unable to add node with pos ["+DhSectionPos.toString(sectionPos)+"] to tree root ["+rootNode+"].");
}
return quadNode;
}
/** @return true if the node at this position has uploaded its render data */
private boolean recursivelyUpdateRenderSectionNode(
@NotNull DhBlockPos2D playerPos,
@@ -520,13 +515,15 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
quadNode = this.tryAddNodeToTree(rootNode, quadNode, sectionPos);
//// Skip sections that are out-of-bounds.
//// If not done some sections will appear and/or generate
//// outside the desired render distance
//if (!this.isSectionPosInBounds(quadNode.sectionPos))
//{
// return true;
//}
// Skip sections that are out-of-bounds.
// If not done some sections will appear and/or generate
// outside the desired render distance
if (!this.isSectionPosInBounds(quadNode.sectionPos))
{
this.tickNodeHolder.addDisableNode(quadNode);
this.recursivelyDisableChildNodes(quadNode);
return true;
}
// make sure the render section is created (shouldn't be necessary, but just in case)
@@ -629,6 +626,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
{
// not all child positions are loaded yet, this one should be rendered instead
this.tickNodeHolder.addEnableNode(quadNode);
this.recursivelyDisableChildNodes(quadNode);
}
else
{
@@ -639,6 +637,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
return nodeCanRender;
}
}
/** @return true if the node at this position has uploaded its render data */
private boolean onDesiredDetailLevel(
@NotNull QuadNode<LodRenderSection> quadNode,
@@ -655,16 +654,56 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
if (quadNode.value != null
&& quadNode.value.gpuUploadComplete())
{
this.tickNodeHolder.addEnableDeleteChildrenNode(quadNode);
return true;
if (!this.tickNodeHolder.getEnabledNodes().contains(parentNode))
this.tickNodeHolder.addEnableDeleteChildrenNode(quadNode);
else
this.tickNodeHolder.addDisableNode(quadNode);
return true; // TODO broken, will enable sections even if parent is enabled
}
else
{
this.tickNodeHolder.addDisableNode(quadNode);
return false;
}
}
@NotNull
private QuadNode<LodRenderSection> tryAddNodeToTree(
@NotNull QuadNode<LodRenderSection> rootNode,
@Nullable QuadNode<LodRenderSection> quadNode,
long sectionPos // section pos is needed here since the quad node may be null
)
{
// create the node
if (quadNode == null)
{
rootNode.setValue(sectionPos, new LodRenderSection(sectionPos, this, this.level, this.fullDataSourceProvider));
quadNode = rootNode.getNode(sectionPos);
}
if (quadNode == null)
{
LodUtil.assertNotReach("Unable to add node with pos ["+DhSectionPos.toString(sectionPos)+"] to tree root ["+rootNode+"].");
}
return quadNode;
}
private void recursivelyDisableChildNodes(@NotNull QuadNode<LodRenderSection> quadNode)
{
for (int i = 0; i < 4; i++)
{
QuadNode<LodRenderSection> childNode = quadNode.getChildByIndex(i);
this.tickNodeHolder.removeEnableAndDisableNode(childNode);
if (childNode != null)
{
this.recursivelyDisableChildNodes(childNode);
}
}
}
//endregion
//=====================//
@@ -34,11 +34,8 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.renderer.AbstractDebugWireframeRenderer;
import com.seibel.distanthorizons.core.render.renderer.BeaconRenderHandler;
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.LodBufferContainer;
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
@@ -48,10 +45,8 @@ import org.jetbrains.annotations.Nullable;
import javax.annotation.WillNotClose;
import java.awt.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
/**
* A render section represents an area that could be rendered.
@@ -75,8 +70,16 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
private boolean renderingEnabled = false;
private boolean beaconsRendering = false;
public boolean retreivedMissingSectionsForRetreival = false;
/**
* Used when a node goes out of render distance
* but isn't removed from the underlying quad tree structure. <br><br>
*
* In those cases we should act as if the node was removed
* for cached render data caching purposes, but not
* for re-creating missing nodes.
*/
public boolean renderDataDirty = false;
public boolean queuedMissingSectionsForRetrieval = false;
/** this reference is necessary so we can determine what VBO to render */
public LodBufferContainer renderBufferContainer;
@@ -320,6 +323,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
// upload complete
this.renderBufferContainer = buffer.buffersUploaded ? buffer : null;
this.renderDataDirty = false;
if (previousContainer != null)
{
@@ -345,7 +349,12 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
//=================//
//region
public boolean gpuUploadComplete() { return this.renderBufferContainer != null; }
/** aka "canRender()" */
public boolean gpuUploadComplete()
{
return this.renderBufferContainer != null
&& !this.renderDataDirty;
}
public boolean getRenderingEnabled() { return this.renderingEnabled; }
public void setRenderingEnabled(boolean enabled) { this.renderingEnabled = enabled;}
@@ -63,21 +63,39 @@ public class QuadTreeTickNodeHolder
{
if(this.presentNodes.add(node))
{
// not a big fan of having to check every node to prevent overlaps, but it does work
this.nodesToEnable.removeIf((QuadNode<LodRenderSection> checkNode) ->
// in James testing as of 4-21-2026
// this should no longer be needed to prevent overlaps,
// however I'm keeping it here as a quick fix solution if
// the problem comes up again
if (false)
{
boolean contained = DhSectionPos.contains(node.sectionPos, checkNode.sectionPos);
if (contained)
// not a big fan of having to check every node to prevent overlaps, but it does work
this.nodesToEnable.removeIf((QuadNode<LodRenderSection> checkNode) ->
{
this.nodesToDisable.add(checkNode);
}
return contained;
});
boolean contained = DhSectionPos.contains(node.sectionPos, checkNode.sectionPos);
if (contained)
{
this.nodesToDisable.add(checkNode);
}
return contained;
});
}
this.nodesToEnable.add(node);
}
}
/** */
public void removeEnableAndDisableNode(QuadNode<LodRenderSection> node)
{
this.nodesToEnable.remove(node);
this.nodesToEnableDeleteChildrenList.remove(node);
this.presentNodes.add(node); // should already be present, but re-added just in case
this.nodesToDisable.add(node); // node shouldn't be rendered since it's being disabled by a parent
}
public HashSet<QuadNode<LodRenderSection>> getEnabledNodes() { return this.nodesToEnable; }
@@ -19,10 +19,12 @@
package com.seibel.distanthorizons.core.util.objects.quadTree;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.QuadTree.LodQuadTree;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import com.seibel.distanthorizons.coreapi.util.MathUtil;
@@ -61,6 +63,11 @@ public class QuadTree<T>
private final MovableGridRingList<QuadNode<T>> topRingList;
private DhBlockPos2D centerBlockPos;
/**
* defines how many blocks the center needs to move in blocks
* before we check for out-of-bound nodes.
*/
private int blockDistanceForNodeClearing = FullDataSourceV2.WIDTH;
@@ -354,35 +361,103 @@ public class QuadTree<T>
//================//
//region
public void setCenterBlockPos(DhBlockPos2D newCenterPos) { this.setCenterBlockPos(newCenterPos, null); }
public void setCenterBlockPos(DhBlockPos2D newCenterPos, @Nullable Consumer<? super T> removedItemConsumer)
public void setCenterBlockPos(DhBlockPos2D newCenterPos) { this.setCenterBlockPos(newCenterPos, null, null); }
/**
* @param removedConsumer fired when a root node is completely removed from the underlying data structure
* @param mutateOutOfBoundConsumer fired when a child node is out of bounds, but not removed from the underlying data structure
*/
public void setCenterBlockPos(
DhBlockPos2D newCenterPos,
@Nullable Consumer<? super T> removedConsumer,
@Nullable Consumer<? super T> mutateOutOfBoundConsumer)
{
this.centerBlockPos = newCenterPos;
MovableGridRingList.Pos2D expectedCenterPos = new MovableGridRingList.Pos2D(
BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.x, this.treeRootDetailLevel),
BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.z, this.treeRootDetailLevel));
if (this.topRingList.getCenter().equals(expectedCenterPos))
// did we move significantly?
boolean ringListMoved = false;
int newCenterPosX = BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.x, this.treeRootDetailLevel);
int newCenterPosZ = BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.z, this.treeRootDetailLevel);
if (this.topRingList.getCenter().getX() == newCenterPosX
&& this.topRingList.getCenter().getY() == newCenterPosZ)
{
// tree doesn't need to be moved
ringListMoved = true;
}
// did we move a little bit?
boolean recalculateOutOfBoundNodes = false;
int centerBlockDistance = this.centerBlockPos.manhattanDist(newCenterPos);
if (centerBlockDistance < this.blockDistanceForNodeClearing)
{
recalculateOutOfBoundNodes = true;
}
if (!ringListMoved
&& !recalculateOutOfBoundNodes)
{
// the tree didn't move enough that we need
// to re-calculate anything
return;
}
// remove out of bounds root nodes
this.topRingList.moveTo(expectedCenterPos.getX(), expectedCenterPos.getY(), (quadNode) ->
this.centerBlockPos = newCenterPos;
// remove out of bound root nodes
this.topRingList.moveTo(newCenterPosX, newCenterPosZ, (quadNode) ->
{
if (quadNode != null)
{
quadNode.deleteAllChildren(removedItemConsumer);
quadNode.deleteAllChildren(removedConsumer);
if (removedItemConsumer != null)
if (removedConsumer != null)
{
removedItemConsumer.accept(quadNode.value);
removedConsumer.accept(quadNode.value);
}
}
});
// mutate out of bound child nodes
this.topRingList.forEach((rootNode) ->
{
this.mutateOutOfBoundChildNodes(rootNode, mutateOutOfBoundConsumer);
});
}
/**
* we don't want to actually remove nodes or node data
* since that can cause the {@link LodQuadTree} to
* flash low-detail LODs when moving into previously-loaded
* LODs, which is really disorienting.
*/
private void mutateOutOfBoundChildNodes(@Nullable QuadNode<T> quadNode, @Nullable Consumer<? super T> mutateOutOfBoundConsumer)
{
// nodes shouldn't be null, but just in case
if (quadNode == null)
{
return;
}
// go over each child node
for (int i = 0; i < 4; i++)
{
QuadNode<T> childNode = quadNode.getChildByIndex(i);
if (childNode == null
|| childNode.value == null)
{
// no need to go any deeper if this node is already empty
continue;
}
// mutate nodes from the bottom up
this.mutateOutOfBoundChildNodes(childNode, mutateOutOfBoundConsumer);
// mutate this node if out of bounds
if (!this.isSectionPosInBounds(childNode.sectionPos))
{
if (mutateOutOfBoundConsumer != null)
{
mutateOutOfBoundConsumer.accept(childNode.value);
}
}
}
}
public final DhBlockPos2D getCenterBlockPos() { return this.centerBlockPos; }
@@ -487,7 +562,7 @@ public class QuadTree<T>
private class QuadTreeNodeIterator implements Iterator<QuadNode<T>>
{
private final QuadTreeRootPosIterator rootNodeIterator;
private final QuadTreeRootPosIterator rootNodePosIterator;
private Iterator<QuadNode<T>> currentNodeIterator;
private QuadNode<T> lastNode = null;
@@ -500,7 +575,7 @@ public class QuadTree<T>
public QuadTreeNodeIterator(boolean onlyReturnLeaves, @Nullable INodeIteratorStoppingFunc<T> stopIteratingFunc)
{
this.rootNodeIterator = new QuadTreeRootPosIterator(false, stopIteratingFunc);
this.rootNodePosIterator = new QuadTreeRootPosIterator(false, stopIteratingFunc);
this.onlyReturnLeaves = onlyReturnLeaves;
this.stopIteratingFunc = stopIteratingFunc;
@@ -511,7 +586,7 @@ public class QuadTree<T>
@Override
public boolean hasNext()
{
if (!this.rootNodeIterator.hasNext()
if (!this.rootNodePosIterator.hasNext()
&& this.currentNodeIterator != null
&& !this.currentNodeIterator.hasNext())
{
@@ -547,9 +622,9 @@ public class QuadTree<T>
{
Iterator<QuadNode<T>> nodeIterator = null;
while ((nodeIterator == null || !nodeIterator.hasNext())
&& this.rootNodeIterator.hasNext())
&& this.rootNodePosIterator.hasNext())
{
long sectionPos = this.rootNodeIterator.nextLong();
long sectionPos = this.rootNodePosIterator.nextLong();
// try-get to prevent concurrency errors if the tree is being moved while we walk through it
QuadNode<T> rootNode = QuadTree.this.tryGetNode(sectionPos);