From d90361af59d231e77b5ff9e65000c52fb53c513e Mon Sep 17 00:00:00 2001 From: James Seibel Date: Sat, 24 Jan 2026 13:37:23 -0600 Subject: [PATCH] Change LOD loading to start at lowest detail --- .../core/level/ClientLevelModule.java | 2 +- .../core/render/LodQuadTree.java | 661 ++++++++++-------- .../core/render/LodRenderSection.java | 168 ++--- .../QuadTree/QuadTreeTickNodeHolder.java | 134 ++++ .../core/render/RenderBufferHandler.java | 42 +- .../core/util/objects/quadTree/QuadNode.java | 15 +- 6 files changed, 621 insertions(+), 401 deletions(-) create mode 100644 core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/QuadTreeTickNodeHolder.java diff --git a/core/src/main/java/com/seibel/distanthorizons/core/level/ClientLevelModule.java b/core/src/main/java/com/seibel/distanthorizons/core/level/ClientLevelModule.java index ba8099a52..5c169cdd6 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/level/ClientLevelModule.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/level/ClientLevelModule.java @@ -112,7 +112,7 @@ public class ClientLevelModule implements Closeable, IDataSourceUpdateListenerFu } } - clientRenderState.quadtree.tick(new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos())); + clientRenderState.quadtree.tryTick(new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos())); } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/LodQuadTree.java b/core/src/main/java/com/seibel/distanthorizons/core/render/LodQuadTree.java index 20733ea17..dc9867f73 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/LodQuadTree.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/LodQuadTree.java @@ -21,7 +21,6 @@ package com.seibel.distanthorizons.core.render; import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.config.listeners.IConfigListener; -import com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding.LodBufferContainer; import com.seibel.distanthorizons.core.enums.EDhDirection; import com.seibel.distanthorizons.core.file.fullDatafile.V2.FullDataSourceProviderV2; import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult; @@ -31,6 +30,7 @@ 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.QuadTreeTickNodeHolder; import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable; import com.seibel.distanthorizons.core.render.renderer.generic.BeaconRenderHandler; @@ -44,6 +44,7 @@ import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import com.seibel.distanthorizons.coreapi.util.MathUtil; import it.unimi.dsi.fastutil.longs.LongArrayList; import it.unimi.dsi.fastutil.longs.LongIterator; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.annotation.WillNotClose; @@ -85,10 +86,6 @@ public class LodQuadTree extends QuadTree implements IDebugRen */ private final ReentrantLock treeLock = new ReentrantLock(); - private ArrayList debugRenderSections = new ArrayList<>(); - private ArrayList altDebugRenderSections = new ArrayList<>(); - private final ReentrantLock debugRenderSectionLock = new ReentrantLock(); - /** * Used to limit how many upload tasks are queued at once. * If all the upload tasks are queued at once, they will start uploading nearest @@ -119,6 +116,16 @@ public class LodQuadTree extends QuadTree implements IDebugRen private final Set queuedGenerationPosSet = Collections.newSetFromMap(new ConcurrentHashMap<>()); /** cached array to prevent having to re-allocate it each tick */ private final ArrayList sortedMissingPosList = new ArrayList<>(); + private final ArrayList debugNodeList = new ArrayList<>(); + /** cached to prevent re-allocating each tick */ + private final QuadTreeTickNodeHolder tickNodeHolder = new QuadTreeTickNodeHolder(); + + /** list of sections that should be rendered */ + private ArrayList enabledSections = new ArrayList<>(); + /** alternate list for thread safety */ + private ArrayList altEnabledSections = new ArrayList<>(); + /** This lock should be very quick since it will be used on the render thread */ + private final ReentrantLock enabledRenderSectionLock = new ReentrantLock(); @@ -151,17 +158,41 @@ public class LodQuadTree extends QuadTree implements IDebugRen + //==================// + // property getters // + //==================// + //region + + public void populateListWithEnabledRenderSections(ArrayList tempProcessNodeList) + { + try + { + // lock for thread safety + this.enabledRenderSectionLock.lock(); + + tempProcessNodeList.clear(); + tempProcessNodeList.addAll(this.enabledSections); + } + finally + { + this.enabledRenderSectionLock.unlock(); + } + } + + //endregion + + + //=============// // tick update // //=============// //region tick update - /** - * This function updates the quadTree based on the playerPos and the current game configs (static and global) - * - * @param playerPos the reference position for the player + /** + * update the quadTree using the playerPos + * and queue any necessary work based on the tree's state. */ - public void tick(DhBlockPos2D playerPos) + public void tryTick(DhBlockPos2D playerPos) { if (this.level == null) { @@ -180,19 +211,6 @@ public class LodQuadTree extends QuadTree implements IDebugRen try { - // recenter if necessary... - this.setCenterBlockPos(playerPos, (renderSection) -> - { - //...removing out of bounds sections - if (renderSection != null) - { - this.fullDataSourceProvider.removeRetrievalRequestIf((long genPos) -> DhSectionPos.contains(renderSection.pos, genPos)); - this.missingGenerationPosSet.remove(renderSection.pos); - this.queuedGenerationPosSet.remove(renderSection.pos); - renderSection.close(); - } - }); - this.updateAllRenderSections(playerPos); } catch (Exception e) @@ -207,29 +225,37 @@ public class LodQuadTree extends QuadTree implements IDebugRen } private void updateAllRenderSections(DhBlockPos2D playerPos) { - if (Config.Client.Advanced.Debugging.DebugWireframe.showQuadTreeRenderStatus.get()) + // this data will be updated as we walk through the tree + this.tickNodeHolder.clear(); + + + //===================// + // recenter the tree // + //===================// + //region + + this.setCenterBlockPos(playerPos, (renderSection) -> { - try + // removing out of bounds sections + if (renderSection != null) { - // lock to prevent accidentally rendering an array that's being populated/cleared - this.debugRenderSectionLock.lock(); - - // swap the debug arrays - this.debugRenderSections.clear(); - ArrayList temp = this.debugRenderSections; - this.debugRenderSections = this.altDebugRenderSections; - this.altDebugRenderSections = temp; + this.fullDataSourceProvider.removeRetrievalRequestIf((long genPos) -> DhSectionPos.contains(renderSection.pos, genPos)); + this.missingGenerationPosSet.remove(renderSection.pos); + this.queuedGenerationPosSet.remove(renderSection.pos); + renderSection.close(); } - finally - { - this.debugRenderSectionLock.unlock(); - } - } + }); + + //endregion + //=======================// + // walk through the tree // + //=======================// + //region + // walk through each root node - HashSet nodesNeedingLoading = new HashSet<>(); LongIterator rootPosIterator = this.rootNodePosIterator(); while (rootPosIterator.hasNext()) { @@ -242,38 +268,37 @@ public class LodQuadTree extends QuadTree implements IDebugRen QuadNode rootNode = this.getNode(rootPos); LodUtil.assertTrue(rootNode != null, "All root nodes should have been created by this point."); - this.recursivelyUpdateRenderSectionNode(playerPos, rootNode, rootNode, rootNode.sectionPos, false, nodesNeedingLoading); + this.recursivelyUpdateRenderSectionNode( + playerPos, + rootNode, null, rootNode, rootNode.sectionPos); } + //endregion - // requeue everything if needed - if (this.requeueAllRetrievalTasksRef.get() - && !this.queueThreadRunningRef.get()) + + + //============// + // queue work // + //============// + //region + + if (this.requeueAllRetrievalTasksRef.getAndSet(false)) { - this.queueThreadRunningRef.set(true); - this.requeueAllRetrievalTasksRef.set(false); - - // running on a separate thread allows for faster loading - // of finished LODs - FULL_DATA_RETRIEVAL_QUEUE_THREAD.execute(() -> + Iterator> nodeIterator = this.nodeIterator(); + while (nodeIterator.hasNext()) { - try + QuadNode node = nodeIterator.next(); + if (node == null || node.value == null) { - this.checkAllNodesForRetrievalRequests(); + continue; } - catch (Exception e) - { - LOGGER.error("Unexpected error getting new queued retrieval tasks, error: [" + e.getMessage() + "].", e); - } - finally - { - this.queueThreadRunningRef.set(false); - } - }); + + node.value.retreivedMissingSectionsForRetreival = false; + } } // queue full data retrieval (world gen) requests if needed - if (this.missingGenerationPosSet.size() != 0 // + if (this.missingGenerationPosSet.size() != 0 // TODO can stay empty if generation is toggled at the wrong time (IE world gen starts, turn it off, then turn it back on) && this.fullDataSourceProvider.canQueueRetrievalNow() && !this.queueThreadRunningRef.get()) { @@ -298,37 +323,170 @@ public class LodQuadTree extends QuadTree implements IDebugRen }); } - // reloading is for sections that have been loaded once already this.reloadQueuedSections(); // loading is for sections that haven't rendered yet - this.loadQueuedSections(playerPos, nodesNeedingLoading); + this.loadQueuedSections(playerPos, this.tickNodeHolder.getLoadSections()); + + //endregion + + + + //==================// + // enable rendering // + //==================// + //region + + this.altEnabledSections.clear(); + + for (QuadNode node : this.tickNodeHolder.getEnabledNodes()) + { + // shouldn't happen, but just in case + if (node == null || node.value == null) { continue; } + + node.value.setRenderingEnabled(true); + this.altEnabledSections.add(node.value); + } + for (QuadNode node : this.tickNodeHolder.getEnableDeleteChildrenNodes()) + { + if (node == null || node.value == null) { continue; } + + node.value.setRenderingEnabled(true); + this.altEnabledSections.add(node.value); + } + + //endregion + + + + //====================// + // update render list // + //====================// + //region + + try + { + this.enabledRenderSectionLock.lock(); + + ArrayList temp = this.enabledSections; + this.enabledSections = this.altEnabledSections; + this.altEnabledSections = temp; + } + finally + { + this.enabledRenderSectionLock.unlock(); + } + + //endregion + + + + //=========================// + // node disabling/deletion // + //=========================// + //region + + // also handles disabling beacons + + for (QuadNode node : this.tickNodeHolder.getDisableNodes()) + { + if (node == null || node.value == null) { continue; } + + node.value.setRenderingEnabled(false); + node.value.tryDisableBeacons(); + } + + // limit the number of world gen tasks we can queue per tick, + // for some LOD sections this can be a very slow process, slowing down the load speed + int maxQueuesPerTick = 20; + int queuesThisTick = 0; + for (QuadNode node : this.tickNodeHolder.getEnableDeleteChildrenNodesNearToFar(playerPos)) + { + if (node == null || node.value == null) { continue; } + + node.deleteAllChildren((childRenderSection) -> + { + if (childRenderSection != null) + { + childRenderSection.setRenderingEnabled(false); + childRenderSection.tryDisableBeacons(); + childRenderSection.close(); + } + }); + + if (this.tickNodeHolder.getLoadSections().size() == 0 + && queuesThisTick < maxQueuesPerTick) + { + // since this section wants to render + // check if it needs any generation to do so + if (!node.value.retreivedMissingSectionsForRetreival) + { + node.value.retreivedMissingSectionsForRetreival = true; + this.tryQueuePosForRetrieval(node.value.pos); + + queuesThisTick++; + } + } + } + + //endregion + + + + //=================// + // beacon enabling // + //=================// + //region + + // must be handled after beacon disabling + // otherwise the beacons will be missing + + for (QuadNode node : this.tickNodeHolder.getEnabledNodes()) + { + if (node == null || node.value == null) { continue; } + + node.value.tryEnableBeacons(); + } + for (QuadNode node : this.tickNodeHolder.getEnableDeleteChildrenNodes()) + { + if (node == null || node.value == null) { continue; } + + node.value.tryEnableBeacons(); + } + + //endregion } - /** @return whether the current position is able to render (note: not if it IS rendering, just if it is ABLE to.) */ - private boolean recursivelyUpdateRenderSectionNode( - DhBlockPos2D playerPos, - QuadNode rootNode, QuadNode quadNode, long sectionPos, - boolean parentSectionIsRendering, - HashSet nodesNeedingLoading) + + //=========================// + // tick - recursive update // + //=========================// + ///region + + private void recursivelyUpdateRenderSectionNode( + @NotNull DhBlockPos2D playerPos, + @NotNull QuadNode rootNode, + @Nullable QuadNode parentNode, + @Nullable QuadNode quadNode, + long sectionPos // section pos is needed here since the quad node may be null + ) { //=====================// // get/create the node // // and render section // //=====================// + ///region // create the node - if (quadNode == null - && this.isSectionPosInBounds(sectionPos)) // the position bounds should only fail when at the edge of the user's render distance - { + if (quadNode == null) + { rootNode.setValue(sectionPos, new LodRenderSection(sectionPos, this, this.level, this.fullDataSourceProvider, this.uploadTaskCountRef)); quadNode = rootNode.getNode(sectionPos); } if (quadNode == null) { - // this node must be out of bounds, or there was an issue adding it to the tree - return false; + LodUtil.assertNotReach("Unable to add node with pos ["+DhSectionPos.toString(sectionPos)+"] to tree root ["+rootNode+"]."); } // make sure the render section is created (shouldn't be necessary, but just in case) @@ -339,169 +497,124 @@ public class LodQuadTree extends QuadTree implements IDebugRen quadNode.setValue(sectionPos, renderSection); } + ///endregion + //===============================// // handle enabling, loading, // // and disabling render sections // //===============================// + ///region - byte expectedDetailLevel = this.calculateExpectedDetailLevel(playerPos, sectionPos); + // load every node for rendering + if (!renderSection.gpuUploadInProgress() + && !renderSection.gpuUploadComplete()) + { + this.tickNodeHolder.addLoadSection(renderSection); + } + + + byte expectedDetailLevel = this.calculateExpectedDetailLevel(playerPos, quadNode.sectionPos); expectedDetailLevel = (byte) Math.min(expectedDetailLevel, this.minRootRenderDetailLevel); expectedDetailLevel += DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL; - if (DhSectionPos.getDetailLevel(sectionPos) > expectedDetailLevel) + if (DhSectionPos.getDetailLevel(quadNode.sectionPos) > expectedDetailLevel) { - //=======================// - // detail level too high // - //=======================// - - boolean thisPosIsRendering = renderSection.getRenderingEnabled(); - boolean allChildrenSectionsAreLoaded = true; - - // recursively update each child render section - for (int i = 0; i < 4; i++) - { - QuadNode childNode = quadNode.getChildByIndex(i); - boolean childSectionLoaded = this.recursivelyUpdateRenderSectionNode(playerPos, rootNode, childNode, DhSectionPos.getChildByIndex(sectionPos, i), thisPosIsRendering || parentSectionIsRendering, nodesNeedingLoading); - allChildrenSectionsAreLoaded = childSectionLoaded && allChildrenSectionsAreLoaded; - } - - - if (!allChildrenSectionsAreLoaded) - { - // not all child positions are loaded yet, or this section is out of render range - return thisPosIsRendering; - } - else - { - // children are all loaded, unload this and parents - - if (renderSection.getRenderingEnabled()) - { - // needs to be fired before the children are enabled so beacons render correctly - renderSection.onRenderingDisabled(); - - - // unload parent sections so they don't become - // outdated when child LODs are updated. - // (They'd have to be reloaded from file anyway during an update) - long parentPos = renderSection.pos; - while (DhSectionPos.getDetailLevel(parentPos) <= this.treeRootDetailLevel) - { - QuadNode parentNode = this.getNode(parentPos); - if (parentNode != null) - { - LodRenderSection parentRenderSection = parentNode.value; - if (parentRenderSection != null) - { - // onRenderDisabled doesn't need to be - // called since these sections shouldn't be loaded - parentRenderSection.setRenderingEnabled(false); - LodBufferContainer buffer = parentRenderSection.bufferContainer; - if (buffer != null) - { - buffer.close(); - parentRenderSection.bufferContainer = null; - } - } - } - - parentPos = DhSectionPos.getParentPos(parentPos); - } - - // this position's rendering has been disabled due to children being rendered - if (Config.Client.Advanced.Debugging.DebugWireframe.showRenderSectionToggling.get()) - { - DebugRenderer.makeParticle(new DebugRenderer.BoxParticle(new DebugRenderer.Box(renderSection.pos, 128f, 156f, 0.09f, Color.WHITE), 0.2, 32f)); - } - } - - - // walk back down the tree and enable each child section - for (int i = 0; i < 4; i++) - { - QuadNode childNode = quadNode.getChildByIndex(i); - this.recursivelyUpdateRenderSectionNode(playerPos, rootNode, childNode, DhSectionPos.getChildByIndex(sectionPos, i), parentSectionIsRendering, nodesNeedingLoading); - } - - // disabling rendering must be done after the children are enabled - // otherwise holes may appear in the world, overlaps are less noticeable - renderSection.setRenderingEnabled(false); - - // this section is now being rendered via its children - return true; - } + this.onDetailLevelTooHigh(playerPos, rootNode, quadNode); } - // TODO this should only equal the expected detail level, the (expectedDetailLevel-1) is a temporary fix to prevent corners from being cut out - else if (DhSectionPos.getDetailLevel(sectionPos) == expectedDetailLevel || DhSectionPos.getDetailLevel(sectionPos) == expectedDetailLevel - 1) + // the (expectedDetailLevel-1) fixes corners being cut out due to distance calculations using the LOD center + else if (DhSectionPos.getDetailLevel(quadNode.sectionPos) == expectedDetailLevel + || DhSectionPos.getDetailLevel(quadNode.sectionPos) == expectedDetailLevel - 1) { - //======================// - // desired detail level // - //======================// - - - // prepare this section for rendering - if (!renderSection.gpuUploadInProgress() - && renderSection.bufferContainer == null) - { - nodesNeedingLoading.add(renderSection); - } - - // update debug if needed - if (Config.Client.Advanced.Debugging.DebugWireframe.showQuadTreeRenderStatus.get()) - { - this.debugRenderSections.add(renderSection); - } - - - - // wait for the parent to disable before enabling this section, so we don't have a hole - if (!parentSectionIsRendering - && renderSection.canRender()) - { - // if rendering is already enabled we don't have to re-enable it - if (!renderSection.getRenderingEnabled()) - { - renderSection.setRenderingEnabled(true); - - // disabling rendering must be done after the parent is enabled - // otherwise holes may appear in the world, overlaps are less noticeable - quadNode.deleteAllChildren((childRenderSection) -> - { - if (childRenderSection != null) - { - if (childRenderSection.getRenderingEnabled()) - { - // this position's rendering has been disabled due to a parent rendering - if (Config.Client.Advanced.Debugging.DebugWireframe.showRenderSectionToggling.get()) - { - DebugRenderer.makeParticle(new DebugRenderer.BoxParticle(new DebugRenderer.Box(childRenderSection.pos, 128f, 156f, 0.09f, Color.MAGENTA), 0.2, 32f)); - } - } - - childRenderSection.setRenderingEnabled(false); - childRenderSection.onRenderingDisabled(); - childRenderSection.close(); - } - }); - - // needs to be fired after the children are disabled so beacons render correctly - renderSection.onRenderingEnabled(); - - // since this section wants to render - // check if it needs any generation to do so - this.tryQueuePosForRetrieval(renderSection.pos); - } - } - - return renderSection.canRender(); + this.onDesiredDetailLevel(quadNode, parentNode); } else { throw new IllegalStateException("LodQuadTree shouldn't be updating renderSections below the expected detail level: [" + expectedDetailLevel + "]."); } + + ///endregion } + private void onDetailLevelTooHigh( + @NotNull DhBlockPos2D playerPos, + @NotNull QuadNode rootNode, @NotNull QuadNode quadNode) + { + // recursively update each child node + boolean allChildNodesCanRender = true; + for (int i = 0; i < 4; i++) + { + QuadNode childNode = quadNode.getChildByIndex(i); + long childPos = DhSectionPos.getChildByIndex(quadNode.sectionPos, i); + this.recursivelyUpdateRenderSectionNode( + playerPos, + rootNode, quadNode, childNode, childPos); + childNode = quadNode.getChildByIndex(i); // needs to be gotten again in case a new node was added to the tree (this will often happen when moving into new areas where the children were deleted) + + // nodes shouldn't be null, but just in case + if (childNode != null + && childNode.value != null + && !childNode.value.gpuUploadComplete()) + { + // the node is present but not uploaded yet + allChildNodesCanRender = false; + } + } + + + if (allChildNodesCanRender) + { + // all child nodes can render, this node isn't needed + this.tickNodeHolder.addDisableNode(quadNode); + } + else + { + // not all child positions are loaded yet, this one should be rendered instead + this.tickNodeHolder.addEnableNode(quadNode); + } + } + private void onDesiredDetailLevel( + @NotNull QuadNode quadNode, @Nullable QuadNode parentNode) + { + boolean allAdjNodesCanRender = true; + + // if the parent node is null, that means we're at the root node + // and we should always render + if (parentNode != null) + { + // check if all adjacent nodes are ready to render + // this check is done to prevent some overlapping due to the parent node + // still being active + for (int i = 0; i < 4; i++) + { + QuadNode adjNode = parentNode.getChildByIndex(i); + // nodes shouldn't be null, but just in case there's an issue + if (adjNode != null + && adjNode.value != null + && !adjNode.value.gpuUploadComplete()) + { + // the node is present but not uploaded yet + allAdjNodesCanRender = false; + } + } + } + + if (allAdjNodesCanRender + && quadNode.value != null + && quadNode.value.gpuUploadComplete()) + { + this.tickNodeHolder.addEnableDeleteChildrenNode(quadNode); + } + } + + + ///endregion + + //=====================// + // tick - work queuing // + //=====================// + //region + private void reloadQueuedSections() { Long pos; @@ -518,7 +631,7 @@ public class LodQuadTree extends QuadTree implements IDebugRen LodRenderSection renderSection = this.tryGetValue(pos); if (renderSection != null) { - if (renderSection.canRender()) + if (renderSection.gpuUploadComplete()) { if (renderSection.gpuUploadInProgress() || !renderSection.uploadRenderDataToGpuAsync()) @@ -536,25 +649,37 @@ public class LodQuadTree extends QuadTree implements IDebugRen } private void loadQueuedSections(DhBlockPos2D playerPos, HashSet nodesNeedingLoading) { + // TODO disable world gen while any tasks exist here to speed up loading speed + ArrayList loadSectionList = new ArrayList<>(nodesNeedingLoading); - loadSectionList.sort((a, b) -> + loadSectionList.sort((LodRenderSection a, LodRenderSection b) -> { + // lower-detail LODs first + byte aDetailLevel = DhSectionPos.getDetailLevel(a.pos); + byte bDetailLevel = DhSectionPos.getDetailLevel(b.pos); + if (aDetailLevel != bDetailLevel) + { + return Byte.compare(bDetailLevel, aDetailLevel); // larger numbers first + } + + // closer LODs first int aDist = DhSectionPos.getManhattanBlockDistance(a.pos, playerPos); int bDist = DhSectionPos.getManhattanBlockDistance(b.pos, playerPos); - return Integer.compare(aDist, bDist); + return Integer.compare(aDist, bDist); // smaller numbers first }); for (int i = 0; i < loadSectionList.size(); i++) { LodRenderSection renderSection = loadSectionList.get(i); if (!renderSection.gpuUploadInProgress() - && renderSection.bufferContainer == null) + && !renderSection.gpuUploadComplete()) { renderSection.uploadRenderDataToGpuAsync(); } } } + //endregion //endregion tick update @@ -679,29 +804,6 @@ public class LodQuadTree extends QuadTree implements IDebugRen } } - /** - * Needed to get all necessary retrieval requests - * after the quad tree has already been loaded. - */ - private void checkAllNodesForRetrievalRequests() - { - Iterator> nodeIterator = this.nodeIterator(); - while (nodeIterator.hasNext()) - { - QuadNode node = nodeIterator.next(); - if (node != null) - { - LodRenderSection renderSection = node.value; - if (renderSection != null - && renderSection.getRenderingEnabled()) - { - this.tryQueuePosForRetrieval(renderSection.pos); - - } - } - } - } - /** Does nothing if the missing positions are already queued. */ private void tryQueuePosForRetrieval(long pos) { @@ -862,45 +964,44 @@ public class LodQuadTree extends QuadTree implements IDebugRen @Override public void debugRender(DebugRenderer debugRenderer) { - try + this.populateListWithEnabledRenderSections(this.debugNodeList); + + for (int i = 0; i < this.debugNodeList.size(); i++) { - // lock to prevent accidentally rendering the array that's being cleared - this.debugRenderSectionLock.lock(); + LodRenderSection renderSection = this.debugNodeList.get(i); - - for (int i = 0; i < this.debugRenderSections.size(); i++) + Color color = Color.BLACK; + if (renderSection.gpuUploadInProgress()) { - LodRenderSection renderSection = this.debugRenderSections.get(i); - - Color color = Color.BLACK; - if (renderSection.gpuUploadInProgress()) - { - color = Color.ORANGE; - } - else if (renderSection.bufferContainer == null) - { - // uploaded but the buffer is missing - color = Color.PINK; - } - else if (renderSection.bufferContainer.hasNonNullVbos()) - { - if (renderSection.bufferContainer.vboBufferCount() != 0) - { - color = Color.GREEN; - } - else - { - // This section is probably rendering an empty chunk - color = Color.RED; - } - } - - debugRenderer.renderBox(new DebugRenderer.Box(renderSection.pos, 400, 400f, Objects.hashCode(this), 0.05f, color)); + color = Color.ORANGE; } - } - finally - { - this.debugRenderSectionLock.unlock(); + else if (!renderSection.gpuUploadComplete()) + { + // uploaded but the buffer is missing + color = Color.PINK; + } + else if (renderSection.renderBufferContainer.hasNonNullVbos()) + { + if (renderSection.renderBufferContainer.vboBufferCount() != 0) + { + color = Color.GREEN; + } + else + { + // This section is probably rendering an empty chunk + color = Color.RED; + } + } + + + int levelMinY = this.level.getLevelWrapper().getMinHeight(); + int levelMaxY = this.level.getLevelWrapper().getMaxHeight(); + // show the wireframe a bit lower than world max height, + // since most worlds don't render all the way up to the max height + int levelHeightRange = (levelMaxY - levelMinY); + int maxY = levelMaxY - (levelHeightRange / 2); + + debugRenderer.renderBox(new DebugRenderer.Box(renderSection.pos, levelMinY, maxY, 0.05f, color)); } } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java b/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java index b10fac25e..cebeeaa38 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java @@ -85,9 +85,11 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable private boolean renderingEnabled = false; + private boolean beaconsRendering = false; + public boolean retreivedMissingSectionsForRetreival = false; /** this reference is necessary so we can determine what VBO to render */ - public LodBufferContainer bufferContainer; + public LodBufferContainer renderBufferContainer; /** @@ -95,8 +97,6 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable * up to the point when geometry data is uploaded to the GPU. */ private CompletableFuture getAndBuildRenderDataFuture = null; - @Nullable - public CompletableFuture getRenderDataBuildFuture() { return this.getAndBuildRenderDataFuture; } /** * used alongside {@link LodRenderSection#getAndBuildRenderDataFuture} so we can remove @@ -191,7 +191,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable this.getAndBuildRenderDataRunnable = () -> { - this.getAndRefreshRenderingBeacons(); + this.refreshActiveBeaconList(); this.getAndUploadRenderDataToGpuAsync() .thenRun(() -> { @@ -363,10 +363,10 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable this.bufferUploadFuture.thenAccept((buffer) -> { // needed to clean up the old data - LodBufferContainer previousContainer = this.bufferContainer; + LodBufferContainer previousContainer = this.renderBufferContainer; // upload complete - this.bufferContainer = buffer.buffersUploaded ? buffer : null; + this.renderBufferContainer = buffer.buffersUploaded ? buffer : null; this.getAndBuildRenderDataFuture = null; if (previousContainer != null) @@ -380,29 +380,63 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable - //====================// - // enabling rendering // - //====================// - //region enabling rendering + //=================// + // rendering state // + //=================// + //region - public boolean canRender() { return this.bufferContainer != null; } + public boolean gpuUploadComplete() { return this.renderBufferContainer != null; } public boolean getRenderingEnabled() { return this.renderingEnabled; } - /** - * Separate from {@link LodRenderSection#onRenderingEnabled} and {@link LodRenderSection#onRenderingDisabled} - * since we need to trigger external changes in disabled -> enabled order - * so beacons are removed and then re-added. - * However, to prevent holes in the world when disabling sections we need to - * enable the new section(s) first before disabling the old one(s). - */ public void setRenderingEnabled(boolean enabled) { this.renderingEnabled = enabled;} - /** @see LodRenderSection#setRenderingEnabled */ - public void onRenderingEnabled() { this.startRenderingBeacons(); } - /** @see LodRenderSection#setRenderingEnabled */ - public void onRenderingDisabled() + public boolean gpuUploadInProgress() { return this.getAndBuildRenderDataFuture != null; } + + //endregion + + + + //=================// + // beacon handling // + //=================// + //region beacon handling + + /** gets the active beacon list and stops/starts beacon rendering as necessary */ + private void refreshActiveBeaconList() { - this.stopRenderingBeacons(); + // do nothing if beacon rendering or repos are unavailable + if (this.beaconBeamRepo == null + || this.beaconRenderHandler == null) + { + return; + } + + + // Synchronized to prevent two threads for accessing the array at once + synchronized (this.activeBeaconList) + { + List activeBeacons = this.beaconBeamRepo.getAllBeamsForPos(this.pos); + + // swap old and new active beacon list + this.activeBeaconList.clear(); + this.activeBeaconList.addAll(activeBeacons); + } + } + + public void tryDisableBeacons() + { + // do nothing if beacon rendering is unavailable + if (this.beaconRenderHandler == null) + { + return; + } + + if (!this.beaconsRendering) + { + return; + } + this.beaconsRendering = false; + if (Config.Client.Advanced.Debugging.DebugWireframe.showRenderSectionStatus.get()) { @@ -414,58 +448,6 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable ) ); } - } - - public boolean gpuUploadInProgress() { return this.getAndBuildRenderDataFuture != null; } - - //endregion enabling rendering - - - - //=================// - // beacon handling // - //=================// - //region beacon handling - - /** gets the active beacon list and stops/starts beacon rendering as necessary */ - private void getAndRefreshRenderingBeacons() - { - // do nothing if beacon rendering or repos are unavailable - if (this.beaconBeamRepo == null - || this.beaconRenderHandler == null) - { - return; - } - - - // Synchronized to prevent two threads for starting/stopping rendering at once - // Shouldn't be necessary, but just in case. - synchronized (this.activeBeaconList) - { - List activeBeacons = this.beaconBeamRepo.getAllBeamsForPos(this.pos); - - - // stop rendering current beacons - this.beaconRenderHandler.stopRenderingBeacons(this.activeBeaconList); - - // swap old and new active beacon list - this.activeBeaconList.clear(); - this.activeBeaconList.addAll(activeBeacons); - - // start rendering new beacon list - byte absoluteDetailLevel = (byte)(DhSectionPos.getDetailLevel(this.pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL); - this.beaconRenderHandler.startRenderingBeacons(this.activeBeaconList, absoluteDetailLevel); - } - } - - private void stopRenderingBeacons() - { - // do nothing if beacon rendering is unavailable - if (this.beaconRenderHandler == null) - { - return; - } - synchronized (this.activeBeaconList) { @@ -473,7 +455,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable } } - private void startRenderingBeacons() + public void tryEnableBeacons() { // do nothing if beacon rendering is unavailable if (this.beaconRenderHandler == null) @@ -481,6 +463,12 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable return; } + if (this.beaconsRendering) + { + return; + } + this.beaconsRendering = true; + synchronized (this.activeBeaconList) { @@ -504,18 +492,28 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable Color color = Color.red; if (this.renderingEnabled) { - color = Color.green; + //color = Color.green; + return; } else if (this.getAndBuildRenderDataFuture != null) { color = Color.yellow; } - else if (this.canRender()) + else if (this.gpuUploadComplete()) { - color = Color.cyan; + //color = Color.cyan; + return; } - debugRenderer.renderBox(new DebugRenderer.Box(this.pos, 400, 8f, Objects.hashCode(this), 0.1f, color)); + int levelMinY = this.level.getLevelWrapper().getMinHeight(); + int levelMaxY = this.level.getLevelWrapper().getMaxHeight(); + + // show the wireframe a bit lower than world max height, + // since most worlds don't render all the way up to the max height + int levelHeightRange = (levelMaxY - levelMinY); + int maxY = levelMaxY - (levelHeightRange / 2); + + debugRenderer.renderBox(new DebugRenderer.Box(this.pos, levelMinY, maxY, 0.01f, color)); } @Override @@ -523,7 +521,9 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable { return "pos=[" + DhSectionPos.toString(this.pos) + "] " + "enabled=[" + this.renderingEnabled + "] " + - "uploading=[" + this.gpuUploadInProgress() + "] "; + "canRender=[" + (this.renderBufferContainer != null) + "] " + + "uploading=[" + this.gpuUploadInProgress() + "] " + ; } @Override @@ -543,11 +543,11 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable } - this.stopRenderingBeacons(); + this.tryDisableBeacons(); - if (this.bufferContainer != null) + if (this.renderBufferContainer != null) { - this.bufferContainer.close(); + this.renderBufferContainer.close(); } // removes any in-progress futures since they aren't needed any more 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 new file mode 100644 index 000000000..a201bde52 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/QuadTree/QuadTreeTickNodeHolder.java @@ -0,0 +1,134 @@ +package com.seibel.distanthorizons.core.render.QuadTree; + +import com.seibel.distanthorizons.core.pos.DhSectionPos; +import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; +import com.seibel.distanthorizons.core.render.LodQuadTree; +import com.seibel.distanthorizons.core.render.LodRenderSection; +import com.seibel.distanthorizons.core.util.objects.quadTree.QuadNode; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; + +/** + * Holds all the data retrieved + * while running {@link LodQuadTree#tryTick(DhBlockPos2D)}. + * This allows for running different logic at different times for each node + * based on whether it should be rendered and it's place in the tree. + */ +public class QuadTreeTickNodeHolder +{ + /** Nodes that should be pulled in from the disk */ + private final HashSet sectionsToLoad = new HashSet<>(); + + private final HashSet> presentNodes = new HashSet<>(); + + private final HashSet> nodesToEnable = new HashSet<>(); + private final HashSet> nodesToDisable = new HashSet<>(); + private final ArrayList> nodesToEnableDeleteChildrenList = new ArrayList<>(); + + private final QuadNodeNearComparator quadNodeNearComparator = new QuadNodeNearComparator(); + + + + //=========// + // methods // + //=========// + ///region + + public void clear() + { + this.sectionsToLoad.clear(); + + this.presentNodes.clear(); + + this.nodesToEnable.clear(); + this.nodesToDisable.clear(); + this.nodesToEnableDeleteChildrenList.clear(); + } + + + // loading + public void addLoadSection(LodRenderSection section) { this.sectionsToLoad.add(section); } + public HashSet getLoadSections() { return this.sectionsToLoad; } + + + // enable + public void addEnableNode(QuadNode node) + { + if(this.presentNodes.add(node)) + { + // TODO not a big fan of having to check all nodes to prevent overlaps, but it does work + this.nodesToEnable.removeIf((QuadNode checkNode) -> + { + boolean contained = DhSectionPos.contains(node.sectionPos, checkNode.sectionPos); + if (contained) + { + this.nodesToDisable.add(checkNode); + } + + return contained; + }); + + this.nodesToEnable.add(node); + } + } + public HashSet> getEnabledNodes() { return this.nodesToEnable; } + + + // disable + public void addDisableNode(QuadNode node) + { + if(this.presentNodes.add(node)) + { + this.nodesToDisable.add(node); + } + } + public HashSet> getDisableNodes() { return this.nodesToDisable; } + + + // enable - delete children + public void addEnableDeleteChildrenNode(QuadNode node) + { + if(this.presentNodes.add(node)) + { + this.nodesToEnableDeleteChildrenList.add(node); + } + } + public ArrayList> getEnableDeleteChildrenNodes() { return this.nodesToEnableDeleteChildrenList; } + public ArrayList> getEnableDeleteChildrenNodesNearToFar(DhBlockPos2D centerPos) + { + this.quadNodeNearComparator.centerPos = centerPos; + this.nodesToEnableDeleteChildrenList.sort(this.quadNodeNearComparator); + return this.nodesToEnableDeleteChildrenList; + } + + ///endregion + + + + //================// + // helper classes // + //================// + ///region + + /** orders closest LODs first */ + private static class QuadNodeNearComparator implements Comparator> + { + public DhBlockPos2D centerPos = DhBlockPos2D.ZERO; + + @Override + public int compare(QuadNode nodeA, QuadNode nodeB) + { + // closer LODs first + int aDist = DhSectionPos.getManhattanBlockDistance(nodeA.sectionPos, this.centerPos); + int bDist = DhSectionPos.getManhattanBlockDistance(nodeB.sectionPos, this.centerPos); + return Integer.compare(aDist, bDist); // smaller numbers first + } + } + + ///endregion + + + +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/RenderBufferHandler.java b/core/src/main/java/com/seibel/distanthorizons/core/render/RenderBufferHandler.java index 643a9d791..1eb0b6579 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/RenderBufferHandler.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/RenderBufferHandler.java @@ -35,7 +35,6 @@ import com.seibel.distanthorizons.core.pos.Pos2D; import com.seibel.distanthorizons.core.render.renderer.LodRenderer; import com.seibel.distanthorizons.core.render.renderer.RenderParams; import com.seibel.distanthorizons.core.util.objects.SortedArraySet; -import com.seibel.distanthorizons.core.util.objects.quadTree.QuadNode; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftGLWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor.IIrisAccessor; @@ -45,7 +44,7 @@ import com.seibel.distanthorizons.core.util.math.Vec3d; import org.joml.Matrix4f; import org.joml.Matrix4fc; -import java.util.Iterator; +import java.util.ArrayList; /** * This object tells the {@link LodRenderer} what buffers to render @@ -63,6 +62,8 @@ public class RenderBufferHandler implements AutoCloseable public final LodQuadTree lodQuadTree; private final SortedArraySet loadedNearToFarBuffers; + /** temp array to prevent threading issues and prevent re-allocating the same array each frame */ + private final ArrayList tempProcessNodeList = new ArrayList<>(); private int visibleBufferCount; private int culledBufferCount; @@ -181,17 +182,12 @@ public class RenderBufferHandler implements AutoCloseable } // setup iterator with culling frustum - Iterator> nodeIterator = this.lodQuadTree.nodeIteratorWithStoppingFilter((QuadNode node) -> + this.lodQuadTree.populateListWithEnabledRenderSections(this.tempProcessNodeList); + for (LodRenderSection renderSection : this.tempProcessNodeList) { - if (node == null) - { - return true; - } - - LodRenderSection renderSection = node.value; if (renderSection == null) { - return false; + continue; } @@ -214,40 +210,24 @@ public class RenderBufferHandler implements AutoCloseable this.culledBufferCount++; } - return true; + continue; } } - - return false; } catch (Exception e) { - LOGGER.error("Unexpected issue during culling for node pos: ["+DhSectionPos.toString(node.sectionPos)+"], error: ["+e.getMessage()+"].", e); - // don't cull if there was an unexpected issue - return false; + LOGGER.error("Unexpected issue during culling for node pos: ["+DhSectionPos.toString(renderSection.pos)+"], error: ["+e.getMessage()+"].", e); } - }); - - while (nodeIterator.hasNext()) - { - QuadNode node = nodeIterator.next(); - - long sectionPos = node.sectionPos; - LodRenderSection renderSection = node.value; - if (renderSection == null) - { - continue; - } - try { - LodBufferContainer bufferContainer = renderSection.bufferContainer; - if (bufferContainer == null + LodBufferContainer bufferContainer = renderSection.renderBufferContainer; + if (bufferContainer == null || !renderSection.getRenderingEnabled()) { + // shouldn't happen, but just in case continue; } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadNode.java b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadNode.java index ea0c44ae0..0279acd91 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadNode.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/util/objects/quadTree/QuadNode.java @@ -43,6 +43,7 @@ public class QuadNode * IE the detail levels that the root nodes in the tree are. */ public final byte parentTreeLeafDetailLevel; + @Nullable public T value; @@ -51,24 +52,28 @@ public class QuadNode * index 0
* relative pos (0,0) */ + @Nullable public QuadNode nwChild; /** * North East
* index 1
* relative (1,0) */ + @Nullable public QuadNode neChild; /** * South West
* index 2
* relative (0,1) */ + @Nullable public QuadNode swChild; /** * South East
* index 3
* relative (1,1) */ + @Nullable public QuadNode seChild; @@ -127,18 +132,18 @@ public class QuadNode * * @param child0to3 must be an int between 0 and 3 */ - public QuadNode getChildByIndex(int child0to3) throws IllegalArgumentException + public @Nullable QuadNode getChildByIndex(int child0to3) throws IllegalArgumentException { switch (child0to3) { case 0: - return nwChild; + return this.nwChild; case 1: - return swChild; + return this.swChild; case 2: - return neChild; + return this.neChild; case 3: - return seChild; + return this.seChild; default: throw new IllegalArgumentException("child0to3 must be between 0 and 3");