From 1debd4b8758353b820f0a60ea332bdcc5bc2f1e4 Mon Sep 17 00:00:00 2001 From: James Seibel Date: Tue, 21 Apr 2026 19:49:50 -0500 Subject: [PATCH] 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 --- .../core/render/QuadTree/LodQuadTree.java | 131 ++++++++++++------ .../render/QuadTree/LodRenderSection.java | 25 ++-- .../QuadTree/QuadTreeTickNodeHolder.java | 36 +++-- .../core/util/objects/quadTree/QuadTree.java | 115 ++++++++++++--- 4 files changed, 224 insertions(+), 83 deletions(-) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/LodQuadTree.java b/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/LodQuadTree.java index cb555039a..72cc7e5f9 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/LodQuadTree.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/LodQuadTree.java @@ -246,21 +246,37 @@ public class LodQuadTree extends QuadTree 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 implements IDebugRen continue; } - node.value.retreivedMissingSectionsForRetreival = false; + node.value.queuedMissingSectionsForRetrieval = false; } } @@ -452,9 +468,9 @@ public class LodQuadTree extends QuadTree 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 implements IDebugRen //=========================// //region - @NotNull - private QuadNode tryAddNodeToTree( - @NotNull QuadNode rootNode, - @Nullable QuadNode 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 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 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 implements IDebugRen return nodeCanRender; } } + /** @return true if the node at this position has uploaded its render data */ private boolean onDesiredDetailLevel( @NotNull QuadNode quadNode, @@ -655,16 +654,56 @@ public class LodQuadTree extends QuadTree 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 tryAddNodeToTree( + @NotNull QuadNode rootNode, + @Nullable QuadNode 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 quadNode) + { + for (int i = 0; i < 4; i++) + { + QuadNode childNode = quadNode.getChildByIndex(i); + this.tickNodeHolder.removeEnableAndDisableNode(childNode); + + if (childNode != null) + { + this.recursivelyDisableChildNodes(childNode); + } + } + } + //endregion //=====================// diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/LodRenderSection.java b/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/LodRenderSection.java index 59d7392f1..41e00be7b 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/LodRenderSection.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/LodRenderSection.java @@ -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.

+ * + * 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;} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/QuadTreeTickNodeHolder.java b/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/QuadTreeTickNodeHolder.java index 2e74267b1..db950195a 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/QuadTreeTickNodeHolder.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/QuadTreeTickNodeHolder.java @@ -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 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 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 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> getEnabledNodes() { return this.nodesToEnable; } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadTree.java b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadTree.java index e6ec31034..c383edc78 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadTree.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadTree.java @@ -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 private final MovableGridRingList> 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 //================// //region - public void setCenterBlockPos(DhBlockPos2D newCenterPos) { this.setCenterBlockPos(newCenterPos, null); } - public void setCenterBlockPos(DhBlockPos2D newCenterPos, @Nullable Consumer 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 removedConsumer, + @Nullable Consumer 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 quadNode, @Nullable Consumer 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 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 private class QuadTreeNodeIterator implements Iterator> { - private final QuadTreeRootPosIterator rootNodeIterator; + private final QuadTreeRootPosIterator rootNodePosIterator; private Iterator> currentNodeIterator; private QuadNode lastNode = null; @@ -500,7 +575,7 @@ public class QuadTree public QuadTreeNodeIterator(boolean onlyReturnLeaves, @Nullable INodeIteratorStoppingFunc 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 @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 { Iterator> 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 rootNode = QuadTree.this.tryGetNode(sectionPos);