Change LOD loading to start at lowest detail

This commit is contained in:
James Seibel
2026-01-24 13:37:23 -06:00
parent 37d08b6dfa
commit d90361af59
6 changed files with 621 additions and 401 deletions
@@ -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()));
}
@@ -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<LodRenderSection> implements IDebugRen
*/
private final ReentrantLock treeLock = new ReentrantLock();
private ArrayList<LodRenderSection> debugRenderSections = new ArrayList<>();
private ArrayList<LodRenderSection> 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<LodRenderSection> implements IDebugRen
private final Set<Long> queuedGenerationPosSet = Collections.newSetFromMap(new ConcurrentHashMap<>());
/** cached array to prevent having to re-allocate it each tick */
private final ArrayList<Long> sortedMissingPosList = new ArrayList<>();
private final ArrayList<LodRenderSection> 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<LodRenderSection> enabledSections = new ArrayList<>();
/** alternate list for thread safety */
private ArrayList<LodRenderSection> 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<LodRenderSection> implements IDebugRen
//==================//
// property getters //
//==================//
//region
public void populateListWithEnabledRenderSections(ArrayList<LodRenderSection> 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<LodRenderSection> 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<LodRenderSection> 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<LodRenderSection> 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<LodRenderSection> nodesNeedingLoading = new HashSet<>();
LongIterator rootPosIterator = this.rootNodePosIterator();
while (rootPosIterator.hasNext())
{
@@ -242,38 +268,37 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
QuadNode<LodRenderSection> 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<QuadNode<LodRenderSection>> nodeIterator = this.nodeIterator();
while (nodeIterator.hasNext())
{
try
QuadNode<LodRenderSection> 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<LodRenderSection> 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<LodRenderSection> 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<LodRenderSection> 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<LodRenderSection> 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<LodRenderSection> 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<LodRenderSection> 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<LodRenderSection> node : this.tickNodeHolder.getEnabledNodes())
{
if (node == null || node.value == null) { continue; }
node.value.tryEnableBeacons();
}
for (QuadNode<LodRenderSection> 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<LodRenderSection> rootNode, QuadNode<LodRenderSection> quadNode, long sectionPos,
boolean parentSectionIsRendering,
HashSet<LodRenderSection> nodesNeedingLoading)
//=========================//
// tick - recursive update //
//=========================//
///region
private void recursivelyUpdateRenderSectionNode(
@NotNull DhBlockPos2D playerPos,
@NotNull QuadNode<LodRenderSection> rootNode,
@Nullable QuadNode<LodRenderSection> parentNode,
@Nullable QuadNode<LodRenderSection> 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<LodRenderSection> 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<LodRenderSection> 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<LodRenderSection> 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<LodRenderSection> 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<LodRenderSection> rootNode, @NotNull QuadNode<LodRenderSection> quadNode)
{
// recursively update each child node
boolean allChildNodesCanRender = true;
for (int i = 0; i < 4; i++)
{
QuadNode<LodRenderSection> 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<LodRenderSection> quadNode, @Nullable QuadNode<LodRenderSection> 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<LodRenderSection> 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<LodRenderSection> 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<LodRenderSection> implements IDebugRen
}
private void loadQueuedSections(DhBlockPos2D playerPos, HashSet<LodRenderSection> nodesNeedingLoading)
{
// TODO disable world gen while any tasks exist here to speed up loading speed
ArrayList<LodRenderSection> 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<LodRenderSection> implements IDebugRen
}
}
/**
* Needed to get all necessary retrieval requests
* after the quad tree has already been loaded.
*/
private void checkAllNodesForRetrievalRequests()
{
Iterator<QuadNode<LodRenderSection>> nodeIterator = this.nodeIterator();
while (nodeIterator.hasNext())
{
QuadNode<LodRenderSection> 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<LodRenderSection> 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));
}
}
@@ -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<Void> getAndBuildRenderDataFuture = null;
@Nullable
public CompletableFuture<Void> 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<BeaconBeamDTO> 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<BeaconBeamDTO> 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
@@ -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<LodRenderSection> sectionsToLoad = new HashSet<>();
private final HashSet<QuadNode<LodRenderSection>> presentNodes = new HashSet<>();
private final HashSet<QuadNode<LodRenderSection>> nodesToEnable = new HashSet<>();
private final HashSet<QuadNode<LodRenderSection>> nodesToDisable = new HashSet<>();
private final ArrayList<QuadNode<LodRenderSection>> 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<LodRenderSection> getLoadSections() { return this.sectionsToLoad; }
// enable
public void addEnableNode(QuadNode<LodRenderSection> 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<LodRenderSection> checkNode) ->
{
boolean contained = DhSectionPos.contains(node.sectionPos, checkNode.sectionPos);
if (contained)
{
this.nodesToDisable.add(checkNode);
}
return contained;
});
this.nodesToEnable.add(node);
}
}
public HashSet<QuadNode<LodRenderSection>> getEnabledNodes() { return this.nodesToEnable; }
// disable
public void addDisableNode(QuadNode<LodRenderSection> node)
{
if(this.presentNodes.add(node))
{
this.nodesToDisable.add(node);
}
}
public HashSet<QuadNode<LodRenderSection>> getDisableNodes() { return this.nodesToDisable; }
// enable - delete children
public void addEnableDeleteChildrenNode(QuadNode<LodRenderSection> node)
{
if(this.presentNodes.add(node))
{
this.nodesToEnableDeleteChildrenList.add(node);
}
}
public ArrayList<QuadNode<LodRenderSection>> getEnableDeleteChildrenNodes() { return this.nodesToEnableDeleteChildrenList; }
public ArrayList<QuadNode<LodRenderSection>> 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<QuadNode<LodRenderSection>>
{
public DhBlockPos2D centerPos = DhBlockPos2D.ZERO;
@Override
public int compare(QuadNode<LodRenderSection> nodeA, QuadNode<LodRenderSection> 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
}
@@ -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<LodBufferContainer> loadedNearToFarBuffers;
/** temp array to prevent threading issues and prevent re-allocating the same array each frame */
private final ArrayList<LodRenderSection> tempProcessNodeList = new ArrayList<>();
private int visibleBufferCount;
private int culledBufferCount;
@@ -181,17 +182,12 @@ public class RenderBufferHandler implements AutoCloseable
}
// setup iterator with culling frustum
Iterator<QuadNode<LodRenderSection>> nodeIterator = this.lodQuadTree.nodeIteratorWithStoppingFilter((QuadNode<LodRenderSection> 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<LodRenderSection> 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;
}
@@ -43,6 +43,7 @@ public class QuadNode<T>
* 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<T>
* index 0 <br>
* relative pos (0,0)
*/
@Nullable
public QuadNode<T> nwChild;
/**
* North East <br>
* index 1 <br>
* relative (1,0)
*/
@Nullable
public QuadNode<T> neChild;
/**
* South West <br>
* index 2 <br>
* relative (0,1)
*/
@Nullable
public QuadNode<T> swChild;
/**
* South East <br>
* index 3 <br>
* relative (1,1)
*/
@Nullable
public QuadNode<T> seChild;
@@ -127,18 +132,18 @@ public class QuadNode<T>
*
* @param child0to3 must be an int between 0 and 3
*/
public QuadNode<T> getChildByIndex(int child0to3) throws IllegalArgumentException
public @Nullable QuadNode<T> 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");