Merge the data structure in LodQuadTree and RenderBufferHandler

This prevents needing to sync both data structures, reducing the chance for things to go wrong
This commit is contained in:
James Seibel
2023-03-25 16:18:22 -05:00
parent 7b795af7b3
commit 1b682580fd
5 changed files with 65 additions and 210 deletions
@@ -339,17 +339,16 @@ public class ColumnRenderSource
// Render Methods //
//================//
private void tryBuildBuffer(IDhClientLevel level, LodQuadTree quadTree)
private void tryBuildBuffer(IDhClientLevel level, ColumnRenderSource renderSource)
{
if (this.buildRenderBufferFuture == null && !ColumnRenderBuffer.isBusy() && !this.isEmpty)
{
ColumnRenderSource[] columnRenderSources = new ColumnRenderSource[ELodDirection.ADJ_DIRECTIONS.length];
for (ELodDirection direction : ELodDirection.ADJ_DIRECTIONS)
{
LodRenderSection renderSection = quadTree.getSection(this.sectionPos.getAdjacentPos(direction)); //FIXME: Handle traveling through different detail levels
if (renderSection != null && renderSection.getRenderSource() != null && renderSection.getRenderSource() instanceof ColumnRenderSource)
if (renderSource != null)
{
columnRenderSources[direction.ordinal() - 2] = ((ColumnRenderSource) renderSection.getRenderSource());
columnRenderSources[direction.ordinal() - 2] = renderSource;
//LOGGER.info("attempting to build buffer for: "+renderSection.pos);
}
}
@@ -383,7 +382,7 @@ public class ColumnRenderSource
* @param renderBufferToSwap The slot for swapping in the new buffer.
* @return True if the swap was successful. False if swap is not needed or if it is in progress.
*/
public boolean trySwapRenderBufferAsync(LodQuadTree quadTree, AtomicReference<AbstractRenderBuffer> renderBufferToSwap)
public boolean trySwapRenderBufferAsync(ColumnRenderSource renderSource, AtomicReference<AbstractRenderBuffer> renderBufferToSwap)
{
// prevent swapping the buffer to quickly
if (this.lastNs != -1 && System.nanoTime() - this.lastNs < SWAP_TIMEOUT_IN_NS)
@@ -425,7 +424,7 @@ public class ColumnRenderSource
}
else
{
this.tryBuildBuffer(this.level, quadTree);
this.tryBuildBuffer(this.level, renderSource);
}
}
}
@@ -9,6 +9,7 @@ import com.seibel.lod.core.util.objects.quadTree.QuadNode;
import org.apache.logging.log4j.Logger;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
public class LodRenderSection
{
@@ -16,19 +17,16 @@ public class LodRenderSection
public final DhSectionPos pos;
// TODO create an enum to represent the section's state instead of using magic numbers in the childCount
// states (may not be a complete or correct list): loaded (childCount 4), unloaded (childCount 0), markedForDeletion/markedForFreeing (childCount -1)
private CompletableFuture<ColumnRenderSource> loadFuture;
private boolean isRenderEnabled = false;
// TODO: Should I provide a way to change the render source?
private ColumnRenderSource renderSource;
private ILodRenderSourceProvider renderSourceProvider = null;
public final AtomicReference<AbstractRenderBuffer> abstractRenderBufferRef = new AtomicReference<>();
// Create sub region
public LodRenderSection(DhSectionPos pos) { this.pos = pos; }
@@ -129,6 +127,12 @@ public class LodRenderSection
this.renderSource = null;
}
if (this.abstractRenderBufferRef.get() != null)
{
this.abstractRenderBufferRef.get().close();
this.abstractRenderBufferRef.set(null);
}
if (this.loadFuture != null)
{
this.loadFuture.cancel(true);
@@ -6,40 +6,30 @@ import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.pos.Pos2D;
import com.seibel.lod.core.pos.DhSectionPos;
import com.seibel.lod.core.render.renderer.LodRenderer;
import com.seibel.lod.core.util.BitShiftUtil;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.util.gridList.MovableGridRingList;
import com.seibel.lod.core.util.math.Vec3f;
import com.seibel.lod.core.util.objects.SortedArraySet;
import org.apache.logging.log4j.Logger;
import java.util.Comparator;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/** This object tells the {@link LodRenderer} what buffers to render */
/**
* This object tells the {@link LodRenderer} what buffers to render
* TODO rename this class, maybe RenderBufferOrganizer or something more specific?
*/
public class RenderBufferHandler
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
/** contains all relevant data */
public final LodQuadTree quadTree;
private final MovableGridRingList<RenderBufferNode> renderBufferNodesGridList;
// TODO: Make sorting go into the update loop instead of the render loop as it doesn't need to be done every frame
private SortedArraySet<LoadedRenderBuffer> loadedNearToFarBuffers = null;
public RenderBufferHandler(LodQuadTree quadTree)
{
this.quadTree = quadTree;
Pos2D expectedCenterPos = new Pos2D(
BitShiftUtil.divideByPowerOfTwo(this.quadTree.getCenterBlockPos().x, this.quadTree.treeMaxDetailLevel),
BitShiftUtil.divideByPowerOfTwo(this.quadTree.getCenterBlockPos().z, this.quadTree.treeMaxDetailLevel));
this.renderBufferNodesGridList = new MovableGridRingList<>(quadTree.ringListWidth()/4, expectedCenterPos);
}
public RenderBufferHandler(LodQuadTree lodQuadTree) { this.quadTree = lodQuadTree; }
@@ -107,7 +97,7 @@ public class RenderBufferHandler
}
// Now that we have the axis directions, we can sort the render list
Comparator<LoadedRenderBuffer> sortFarToNear = (loadedBufferA, loadedBufferB) ->
Comparator<LoadedRenderBuffer> farToNearComparator = (loadedBufferA, loadedBufferB) ->
{
Pos2D aPos = loadedBufferA.pos.getCenter().getCenterBlockPos().toPos2D();
Pos2D bPos = loadedBufferB.pos.getCenter().getCenterBlockPos().toPos2D();
@@ -144,16 +134,19 @@ public class RenderBufferHandler
};
// Build the sorted list
this.loadedNearToFarBuffers = new SortedArraySet<>((a, b) -> -sortFarToNear.compare(a, b));
// Add all the loaded buffers to the sorted list
this.renderBufferNodesGridList.forEach((renderBufferNode) ->
this.loadedNearToFarBuffers = new SortedArraySet<>((a, b) -> -farToNearComparator.compare(a, b)); // TODO is the comparator named wrong?
this.quadTree.forEachLeafValue((renderSection, sectionPos) ->
{
if (renderBufferNode != null)
if (renderSection != null && renderSection.shouldRender())
{
renderBufferNode.collect(this.loadedNearToFarBuffers);
// this should always be true
if (renderSection.abstractRenderBufferRef.get() != null)
{
this.loadedNearToFarBuffers.add(new LoadedRenderBuffer(renderSection.abstractRenderBufferRef.get(), sectionPos));
}
}
});
}
public void renderOpaque(LodRenderer renderContext)
@@ -171,52 +164,43 @@ public class RenderBufferHandler
public void update()
{
byte topDetailLevel = this.quadTree.treeMaxDetailLevel;
Pos2D expectedCenterPos = new Pos2D(
BitShiftUtil.divideByPowerOfTwo(this.quadTree.getCenterBlockPos().x, this.quadTree.treeMaxDetailLevel),
BitShiftUtil.divideByPowerOfTwo(this.quadTree.getCenterBlockPos().z, this.quadTree.treeMaxDetailLevel));
this.renderBufferNodesGridList.moveTo(expectedCenterPos.x, expectedCenterPos.y, RenderBufferNode::close); // Note: may lock the list
this.renderBufferNodesGridList.forEachPosOrdered((rootRenderBufferNode, pos2d) ->
this.quadTree.forEachLeafValue((renderSection, sectionPos) ->
{
try
if (renderSection != null)
{
DhSectionPos rootSectionPos = new DhSectionPos(topDetailLevel, pos2d.x, pos2d.y);
LodRenderSection rootRenderSection = this.quadTree.getSection(rootSectionPos);
ColumnRenderSource currentRenderSource = renderSection.getRenderSource();
if (rootRenderSection == null && rootRenderBufferNode != null)
// Update self's render buffer state
if (!renderSection.shouldRender())
{
// section is null, but a node exists, remove the node
this.renderBufferNodesGridList.remove(pos2d).close();
}
else if (rootRenderSection != null)
{
if (rootRenderBufferNode == null)
//TODO: Does this really need to force the old buffer to not be rendered?
AbstractRenderBuffer previousRenderBuffer = renderSection.abstractRenderBufferRef.getAndSet(null);
if (previousRenderBuffer != null)
{
// renderSection exists, but node does not
rootRenderBufferNode = this.renderBufferNodesGridList.setChained(pos2d, new RenderBufferNode(rootSectionPos));
previousRenderBuffer.close();
}
// Update the render node
rootRenderBufferNode.update();
}
}
catch (Exception e)
{
// TODO when we are stable this shouldn't be necessary
LOGGER.error(RenderBufferHandler.class.getSimpleName()+" exception in update for the quadTree: "+this.quadTree+", exception: "+e.getMessage(), e);
int breaker = 0;
else
{
LodUtil.assertTrue(currentRenderSource != null); // section.shouldRender() should have ensured this
currentRenderSource.trySwapRenderBufferAsync(renderSection.getRenderSource(), renderSection.abstractRenderBufferRef);
}
}
});
}
public void close() { this.renderBufferNodesGridList.clear(RenderBufferNode::close); }
public void close()
{
this.quadTree.forEachLeafValue((renderSection) ->
{
if (renderSection.abstractRenderBufferRef.get() != null)
{
renderSection.abstractRenderBufferRef.get().close();
renderSection.abstractRenderBufferRef.set(null);
}
});
}
@@ -236,141 +220,4 @@ public class RenderBufferHandler
}
}
private class RenderBufferNode implements AutoCloseable
{
private final DhSectionPos pos;
private volatile RenderBufferNode[] children = null;
//FIXME: The multiple Atomics will cause race conditions between them!
private final AtomicReference<AbstractRenderBuffer> renderBufferRef = new AtomicReference<>();
public RenderBufferNode(DhSectionPos pos) { this.pos = pos; }
public void collect(SortedArraySet<LoadedRenderBuffer> sortedSet)
{
AbstractRenderBuffer renderBuffer = this.renderBufferRef.get();
if (renderBuffer != null)
{
sortedSet.add(new LoadedRenderBuffer(renderBuffer, this.pos));
}
else
{
RenderBufferNode[] children = this.children;
if (children != null)
{
for (RenderBufferNode child : children)
{
child.collect(sortedSet);
}
}
}
}
//TODO: In the future make this logic a bit more complete so that when children are just created,
// the buffer is only unloaded if all children's buffers are ready. This will make the
// transition between buffers no longer causing any flicker.
public void update()
{
LodRenderSection renderSection = quadTree.getSection(this.pos);
// If this fails, there may be a concurrent modification of the quad tree
// (as this update() should be called from the same thread that calls update() on the quad tree)
LodUtil.assertTrue(renderSection != null, RenderBufferHandler.class.getSimpleName()+" update failed. Expected to find a "+LodRenderSection.class.getSimpleName()+" at pos: "+this.pos+" in the "+LodQuadTree.class.getSimpleName());
ColumnRenderSource currentRenderSource = renderSection.getRenderSource();
// Update self's render buffer state
if (!renderSection.shouldRender())
{
//TODO: Does this really need to force the old buffer to not be rendered?
AbstractRenderBuffer previousRenderBuffer = this.renderBufferRef.getAndSet(null);
if (previousRenderBuffer != null)
{
previousRenderBuffer.close();
}
}
else
{
LodUtil.assertTrue(currentRenderSource != null); // section.shouldRender() should have ensured this
currentRenderSource.trySwapRenderBufferAsync(quadTree, this.renderBufferRef);
}
// Update children's render buffer state
// TODO: Improve this! (Checking section.isLoaded() as if its not loaded, it can only be because
// it has children. (But this logic is... really hard to read!)
// FIXME: Above comment is COMPLETELY WRONG! I am an idiot!
int loadedChildCount = quadTree.getNonNullChildCountAtPos(renderSection.pos);
boolean sectionHasChildren = loadedChildCount != 0;
if (sectionHasChildren)
{
if (this.children == null)
{
RenderBufferNode[] potentialChildren = new RenderBufferNode[loadedChildCount];
AtomicInteger childIndexRef = new AtomicInteger(0);
renderSection.pos.forEachChild((childSectionPos) ->
{
LodRenderSection childRenderSection = quadTree.get(childSectionPos);
if (childRenderSection != null)
{
int i = childIndexRef.get();
potentialChildren[i] = new RenderBufferNode(childSectionPos);
childIndexRef.getAndAdd(1);
}
});
if (childIndexRef.get() != 0)
{
this.children = potentialChildren;
}
}
for (RenderBufferNode child : this.children)
{
child.update();
}
}
else
{
if (this.children != null)
{
//FIXME: Concurrency issue here: If render thread is concurrently using the child's buffer,
// and this thread got priority to close the buffer, it causes a bug where the render thread
// will be using a closed buffer!!!!
RenderBufferNode[] children = this.children;
this.children = null;
for (RenderBufferNode child : children)
{
child.close();
}
}
}
}
@Override
public void close()
{
if (this.children != null)
{
for (RenderBufferNode child : this.children)
{
child.close();
}
}
AbstractRenderBuffer renderBuffer = this.renderBufferRef.getAndSet(null);
if (renderBuffer != null)
{
renderBuffer.close();
}
}
}
}
@@ -262,16 +262,18 @@ public class QuadNode<T>
}
}
/** TODO comment */
public void forAllLeafValues(Consumer<? super T> consumer) { this.forAllLeafValues((value, sectionPos) -> { consumer.accept(value); }); }
/**
* Applies the given consumer to all leaf nodes below this node. <br>
* Note: this will pass in null values.
*/
public void forAllLeafValues(Consumer<? super T> callback)
public void forAllLeafValues(BiConsumer<? super T, DhSectionPos> consumer)
{
if (this.getChildCount() == 0 || this.sectionPos.sectionDetailLevel == this.minimumDetailLevel)
{
// base case, bottom leaf node found
callback.accept(this.value);
consumer.accept(this.value, this.sectionPos);
}
else
{
@@ -281,7 +283,7 @@ public class QuadNode<T>
if (childNode != null)
{
// TODO should this pass in a null value if the child node is null?
childNode.forAllLeafValues(callback);
childNode.forAllLeafValues(consumer);
}
}
}
@@ -201,7 +201,8 @@ public class QuadTree<T>
});
}
public void forEachLeafValue(Consumer<? super T> consumer)
public void forEachLeafValue(Consumer<? super T> consumer) { this.forEachLeafValue((value, sectionPos) -> { consumer.accept(value); }); }
public void forEachLeafValue(BiConsumer<? super T, DhSectionPos> consumer)
{
this.forEachRootNode((rootNode) ->
{
@@ -260,7 +261,9 @@ public class QuadTree<T>
return count.get();
}
public int ringListWidth() { return this.topRingList.getWidth(); }
// TODO comment, currently a tree will always have 9 root nodes, because the tree will grow all the way up to the top, if this is ever changed then these values must also change
public int ringListWidth() { return 3; }
public int ringListHalfWidth() { return 1; }
public int diameterInBlocks() { return this.widthInBlocks; }
// public String getDebugString()