Fix RenderSection overlap and holes

This commit is contained in:
James Seibel
2023-04-17 21:40:45 -05:00
parent 158b7561bc
commit eb5da6fa4d
10 changed files with 358 additions and 416 deletions
@@ -250,7 +250,6 @@ public class ColumnRenderSource
// the source is empty, don't attempt to update anything
return;
}
// the source isn't empty, this object won't be empty after the method finishes
this.isEmpty = false;
@@ -366,10 +365,7 @@ public class ColumnRenderSource
}
}
public void enableRender(IDhClientLevel level)
{
this.level = level;
}
public void allowRendering(IDhClientLevel level) { this.level = level; }
public void disableRender() { this.cancelBuildBuffer(); }
@@ -397,8 +393,16 @@ public class ColumnRenderSource
this.lastNs = System.nanoTime();
//LOGGER.info("Swapping render buffer for {}", sectionPos);
ColumnRenderBuffer newBuffer = this.buildRenderBufferFuture.join();
LodUtil.assertTrue(newBuffer.areBuffersUploaded(), "The buffer future for "+renderSource.sectionPos+" returned an un-built buffer.");
ColumnRenderBuffer oldBuffer = renderBufferRefToSwap.getAndSet(newBuffer);
if (oldBuffer != null)
{
// the old buffer is now considered unloaded, it will need to be freshly re-loaded
oldBuffer.setBuffersUploaded(false);
}
ColumnRenderBuffer swapped = this.columnRenderBufferRef.swap(oldBuffer);
LodUtil.assertTrue(swapped == null);
@@ -2,6 +2,7 @@ package com.seibel.lod.core.dataObjects.render.bufferBuilding;
import com.seibel.lod.core.dataObjects.render.ColumnRenderSource;
import com.seibel.lod.core.dataObjects.render.columnViews.ColumnArrayView;
import com.seibel.lod.core.pos.DhSectionPos;
import com.seibel.lod.core.util.RenderDataPointUtil;
import com.seibel.lod.core.level.IDhClientLevel;
import com.seibel.lod.core.render.renderer.LodRenderer;
@@ -591,6 +592,7 @@ public class ColumnRenderBuffer extends AbstractRenderBuffer
// getters //
//=========//
public void setBuffersUploaded(boolean value) { this.buffersUploaded = value; }
public boolean areBuffersUploaded() { return this.buffersUploaded; }
// TODO move static methods to their own class to avoid confusion
@@ -16,10 +16,10 @@ import java.util.concurrent.CompletableFuture;
*/
public interface ILodRenderSourceProvider extends AutoCloseable
{
CompletableFuture<ColumnRenderSource> read(DhSectionPos pos);
CompletableFuture<ColumnRenderSource> readAsync(DhSectionPos pos);
void addScannedFile(Collection<File> detectedFiles);
void write(DhSectionPos sectionPos, ChunkSizedFullDataSource chunkData);
CompletableFuture<Void> flushAndSave();
void writeChunkDataToFile(DhSectionPos sectionPos, ChunkSizedFullDataSource chunkData);
CompletableFuture<Void> flushAndSaveAsync();
/** Returns true if the data was refreshed, false otherwise */
boolean refreshRenderSource(ColumnRenderSource source);
@@ -117,8 +117,9 @@ public class RenderMetaDataFile extends AbstractMetaDataContainerFile
Object inner = ((SoftReference<?>) obj).get();
if (inner != null)
{
fileHandler.onReadRenderSourceFromCache(this, (ColumnRenderSource) inner);
return CompletableFuture.completedFuture((ColumnRenderSource) inner);
return fileHandler.onReadRenderSourceLoadedFromCacheAsync(this, (ColumnRenderSource) inner)
// wait for the handler to finish before returning the renderSource
.handle((voidObj, ex) -> (ColumnRenderSource) inner);
}
}
@@ -159,7 +160,7 @@ public class RenderMetaDataFile extends AbstractMetaDataContainerFile
// After cas. We are in exclusive control.
if (!this.doesFileExist)
{
this.fileHandler.onCreateRenderFile(this)
this.fileHandler.onCreateRenderFileAsync(this)
.thenApply((data) ->
{
this.metaData = makeMetaData(data);
@@ -54,12 +54,16 @@ public class RenderSourceFileHandler implements ILodRenderSourceProvider
/**
* Caller must ensure that this method is called only once,
* and that the given files are not used before this method is called.
*/
@Override
public void addScannedFile(Collection<File> newRenderFiles)
//===============//
// file handling //
//===============//
/**
* Caller must ensure that this method is called only once,
* and that the given files are not used before this method is called.
*/
@Override
public void addScannedFile(Collection<File> newRenderFiles)
{
HashMultimap<DhSectionPos, RenderMetaDataFile> filesByPos = HashMultimap.create();
@@ -90,7 +94,7 @@ public class RenderSourceFileHandler implements ILodRenderSourceProvider
//fileToUse = metaFiles.stream().findFirst().orElse(null); // use the first file in the list
// use the file's last modified date
fileToUse = Collections.max(metaFiles, Comparator.comparingLong(renderMetaDataFile ->
fileToUse = Collections.max(metaFiles, Comparator.comparingLong(renderMetaDataFile ->
renderMetaDataFile.file.lastModified()));
// fileToUse = Collections.max(metaFiles, Comparator.comparingLong(renderMetaDataFile ->
@@ -111,7 +115,7 @@ public class RenderSourceFileHandler implements ILodRenderSourceProvider
sb.append("\n");
sb.append("(Other files will be renamed by appending \".old\" to their name.)");
LOGGER.warn(sb.toString());
// Rename all other files with the same pos to .old
for (RenderMetaDataFile metaFile : metaFiles)
{
@@ -143,15 +147,9 @@ public class RenderSourceFileHandler implements ILodRenderSourceProvider
}
}
//===============//
// file handling //
//===============//
/** This call is concurrent. I.e. it supports multiple threads calling this method at the same time. */
/** This call is concurrent. I.e. it supports multiple threads calling this method at the same time. */
@Override
public CompletableFuture<ColumnRenderSource> read(DhSectionPos pos)
public CompletableFuture<ColumnRenderSource> readAsync(DhSectionPos pos)
{
RenderMetaDataFile metaFile = this.filesBySectionPos.get(pos);
if (metaFile == null)
@@ -196,14 +194,32 @@ public class RenderSourceFileHandler implements ILodRenderSourceProvider
});
}
/* This call is concurrent. I.e. it supports multiple threads calling this method at the same time. */
@Override
public void write(DhSectionPos sectionPos, ChunkSizedFullDataSource chunkData)
public CompletableFuture<ColumnRenderSource> onCreateRenderFileAsync(RenderMetaDataFile file)
{
this.writeRecursively(sectionPos,chunkData);
this.fullDataSourceProvider.write(sectionPos, chunkData); // TODO why is there fullData handling in the render file handler?
final int vertSize = Config.Client.Graphics.Quality.verticalQuality.get()
.calculateMaxVerticalData((byte) (file.pos.sectionDetailLevel - ColumnRenderSource.SECTION_SIZE_OFFSET));
return CompletableFuture.completedFuture(
new ColumnRenderSource(file.pos, vertSize, this.level.getMinY()));
}
//=============//
// data saving //
//=============//
/**
* This call is concurrent. I.e. it supports multiple threads calling this method at the same time. <br>
* TODO why is there fullData handling in the render file handler?
*/
@Override
public void writeChunkDataToFile(DhSectionPos sectionPos, ChunkSizedFullDataSource chunkData)
{
this.writeChunkDataToFileRecursively(sectionPos,chunkData);
this.fullDataSourceProvider.write(sectionPos, chunkData);
}
private void writeRecursively(DhSectionPos sectPos, ChunkSizedFullDataSource chunkData)
private void writeChunkDataToFileRecursively(DhSectionPos sectPos, ChunkSizedFullDataSource chunkData)
{
if (!sectPos.getSectionBBoxPos().overlapsExactly(new DhLodPos((byte) (4 + chunkData.dataDetail), chunkData.x, chunkData.z)))
{
@@ -213,10 +229,10 @@ public class RenderSourceFileHandler implements ILodRenderSourceProvider
if (sectPos.sectionDetailLevel > ColumnRenderSource.SECTION_SIZE_OFFSET)
{
this.writeRecursively(sectPos.getChildByIndex(0), chunkData);
this.writeRecursively(sectPos.getChildByIndex(1), chunkData);
this.writeRecursively(sectPos.getChildByIndex(2), chunkData);
this.writeRecursively(sectPos.getChildByIndex(3), chunkData);
this.writeChunkDataToFileRecursively(sectPos.getChildByIndex(0), chunkData);
this.writeChunkDataToFileRecursively(sectPos.getChildByIndex(1), chunkData);
this.writeChunkDataToFileRecursively(sectPos.getChildByIndex(2), chunkData);
this.writeChunkDataToFileRecursively(sectPos.getChildByIndex(3), chunkData);
}
RenderMetaDataFile metaFile = this.filesBySectionPos.get(sectPos);
@@ -227,9 +243,10 @@ public class RenderSourceFileHandler implements ILodRenderSourceProvider
}
}
/** This call is concurrent. I.e. it supports multiple threads calling this method at the same time. */
@Override
public CompletableFuture<Void> flushAndSave()
public CompletableFuture<Void> flushAndSaveAsync()
{
LOGGER.info("Shutting down "+ RenderSourceFileHandler.class.getSimpleName()+"...");
@@ -242,36 +259,21 @@ public class RenderSourceFileHandler implements ILodRenderSourceProvider
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
.whenComplete((voidObj, exception) -> LOGGER.info("Finished shutting down "+ RenderSourceFileHandler.class.getSimpleName()) );
}
@Override
public void close()
{
ArrayList<CompletableFuture<Void>> futures = new ArrayList<>();
for (RenderMetaDataFile metaFile : this.filesBySectionPos.values())
{
futures.add(metaFile.flushAndSave(this.renderCacheThread));
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
}
public File computeRenderFilePath(DhSectionPos pos) { return new File(this.saveDir, pos.serialize() + RENDER_FILE_EXTENSION);}
public CompletableFuture<ColumnRenderSource> onCreateRenderFile(RenderMetaDataFile file)
{
final int vertSize = Config.Client.Graphics.Quality.verticalQuality.get()
.calculateMaxVerticalData((byte) (file.pos.sectionDetailLevel - ColumnRenderSource.SECTION_SIZE_OFFSET));
return CompletableFuture.completedFuture(
new ColumnRenderSource(file.pos, vertSize, this.level.getMinY()));
}
private void updateCache(ColumnRenderSource renderSource, RenderMetaDataFile file)
//================//
// cache updating //
//================//
private CompletableFuture<Void> updateCacheAsync(ColumnRenderSource renderSource, RenderMetaDataFile file)
{
if (this.cacheUpdateLockBySectionPos.putIfAbsent(file.pos, new Object()) != null)
{
return;
return CompletableFuture.completedFuture(null);
}
// get the full data source loading future
final WeakReference<ColumnRenderSource> renderSourceReference = new WeakReference<>(renderSource); // TODO why is this a week reference?
CompletableFuture<IFullDataSource> fullDataSourceFuture = this.fullDataSourceProvider.read(renderSource.getSectionPos());
fullDataSourceFuture = fullDataSourceFuture.thenApply((fullDataSource) ->
@@ -289,51 +291,59 @@ public class RenderSourceFileHandler implements ILodRenderSourceProvider
return null;
});
// future returned
CompletableFuture<Void> transformationCompleteFuture = new CompletableFuture<>();
// convert the full data source into a render source
//LOGGER.info("Recreating cache for {}", data.getSectionPos());
DataRenderTransformer.transformDataSourceAsync(fullDataSourceFuture, this.level)
.thenAccept((newRenderSource) -> this.write(renderSourceReference.get(), file, newRenderSource))
.exceptionally((ex) ->
.whenComplete((newRenderSource, ex) ->
{
if (ex instanceof InterruptedException)
if (ex == null)
{
// expected if the transformer is shut down, the exception can be ignored
// LOGGER.warn("RenderSource file transforming interrupted.");
this.writeRenderSourceToFile(renderSourceReference.get(), file, newRenderSource);
}
else if (ex instanceof RejectedExecutionException || ex.getCause() instanceof RejectedExecutionException)
else
{
// expected if the transformer was already shut down, the exception can be ignored
// LOGGER.warn("RenderSource file transforming interrupted.");
}
else if (!UncheckedInterruptedException.isThrowableInterruption(ex))
{
LOGGER.error("Exception when updating render file using data source: ", ex);
if (ex instanceof InterruptedException)
{
// expected if the transformer is shut down, the exception can be ignored
// LOGGER.warn("RenderSource file transforming interrupted.");
int ignoreEmptyWarning = 0; // explicitly handling these exceptions is important so we know where they are going and if there is an issue we can easily re-enable the logging
}
else if (ex instanceof RejectedExecutionException || ex.getCause() instanceof RejectedExecutionException)
{
// expected if the transformer was already shut down, the exception can be ignored
// LOGGER.warn("RenderSource file transforming interrupted.");
int ignoreEmptyWarning = 0;
}
else if (!UncheckedInterruptedException.isThrowableInterruption(ex))
{
LOGGER.error("Exception when updating render file using data source: ", ex);
}
}
return null;
transformationCompleteFuture.complete(null);
})
.thenRun(() -> this.cacheUpdateLockBySectionPos.remove(file.pos));
return transformationCompleteFuture;
}
/** TODO at some point this method may need to be made "async" like {@link RenderSourceFileHandler#onReadRenderSourceLoadedFromCacheAsync} since the insides are async */
public ColumnRenderSource onRenderFileLoaded(ColumnRenderSource renderSource, RenderMetaDataFile file)
{
// if (!this.fullDataSourceProvider.isCacheVersionValid(file.pos, file.metaData.dataVersion.get()))
// {
this.updateCache(renderSource, file);
// }
this.updateCacheAsync(renderSource, file).join();
return renderSource;
}
public void onReadRenderSourceFromCache(RenderMetaDataFile file, ColumnRenderSource data)
{
// if (!this.fullDataSourceProvider.isCacheVersionValid(file.pos, file.metaData.dataVersion.get()))
// {
this.updateCache(data, file);
// }
}
public CompletableFuture<Void> onReadRenderSourceLoadedFromCacheAsync(RenderMetaDataFile file, ColumnRenderSource data) { return this.updateCacheAsync(data, file); }
private void write(ColumnRenderSource currentRenderSource, RenderMetaDataFile file,
ColumnRenderSource newRenderSource)
private void writeRenderSourceToFile(ColumnRenderSource currentRenderSource, RenderMetaDataFile file, ColumnRenderSource newRenderSource)
{
if (currentRenderSource == null || newRenderSource == null)
{
@@ -364,7 +374,7 @@ public class RenderSourceFileHandler implements ILodRenderSourceProvider
LodUtil.assertTrue(file.metaData != null);
// if (!this.fullDataSourceProvider.isCacheVersionValid(file.pos, file.metaData.dataVersion.get()))
// {
this.updateCache(renderSource, file);
this.updateCacheAsync(renderSource, file).join();
return true;
// }
@@ -372,6 +382,23 @@ public class RenderSourceFileHandler implements ILodRenderSourceProvider
}
//=====================//
// clearing / shutdown //
//=====================//
@Override
public void close()
{
ArrayList<CompletableFuture<Void>> futures = new ArrayList<>();
for (RenderMetaDataFile metaFile : this.filesBySectionPos.values())
{
futures.add(metaFile.flushAndSave(this.renderCacheThread));
}
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
}
public void deleteRenderCache()
{
// delete each file in the cache directory
@@ -391,4 +418,13 @@ public class RenderSourceFileHandler implements ILodRenderSourceProvider
this.filesBySectionPos.clear();
}
//================//
// helper methods //
//================//
public File computeRenderFilePath(DhSectionPos pos) { return new File(this.saveDir, pos.serialize() + RENDER_FILE_EXTENSION);}
}
@@ -11,7 +11,6 @@ import com.seibel.lod.core.file.fullDatafile.RemoteFullDataFileHandler;
import com.seibel.lod.core.file.structure.AbstractSaveStructure;
import com.seibel.lod.core.level.states.ClientRenderState;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.logging.f3.F3Screen;
import com.seibel.lod.core.pos.DhBlockPos2D;
import com.seibel.lod.core.pos.DhLodPos;
import com.seibel.lod.core.pos.DhSectionPos;
@@ -21,7 +20,6 @@ import com.seibel.lod.core.util.math.Mat4f;
import com.seibel.lod.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.lod.core.wrapperInterfaces.minecraft.IProfilerWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.lod.core.wrapperInterfaces.world.ILevelWrapper;
import org.apache.logging.log4j.Logger;
@@ -104,7 +102,7 @@ public abstract class AbstractDhClientLevel implements IDhClientLevel
}
clientRenderState.quadtree.tick(new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos()));
clientRenderState.renderer.bufferHandler.update();
clientRenderState.renderer.bufferHandler.updateQuadTreeRenderSources();
return true;
}
@@ -186,7 +184,7 @@ public abstract class AbstractDhClientLevel implements IDhClientLevel
DhLodPos pos = data.getBBoxLodPos().convertToDetailLevel(FullDataSource.SECTION_SIZE_OFFSET);
if (ClientRenderState != null)
{
ClientRenderState.renderSourceFileHandler.write(new DhSectionPos(pos.detailLevel, pos.x, pos.z), data);
ClientRenderState.renderSourceFileHandler.writeChunkDataToFile(new DhSectionPos(pos.detailLevel, pos.x, pos.z), data);
}
else
{
@@ -200,7 +198,7 @@ public abstract class AbstractDhClientLevel implements IDhClientLevel
ClientRenderState ClientRenderState = this.ClientRenderStateRef.get();
if (ClientRenderState != null)
{
return ClientRenderState.renderSourceFileHandler.flushAndSave().thenCombine(this.fullDataFileHandler.flushAndSave(), (voidA, voidB) -> null);
return ClientRenderState.renderSourceFileHandler.flushAndSaveAsync().thenCombine(this.fullDataFileHandler.flushAndSave(), (voidA, voidB) -> null);
}
else
{
@@ -52,7 +52,7 @@ public class ClientRenderState
this.renderer.close();
this.quadtree.close();
return this.renderSourceFileHandler.flushAndSave();
return this.renderSourceFileHandler.flushAndSaveAsync();
}
}
@@ -7,6 +7,7 @@ import com.seibel.lod.core.pos.DhSectionPos;
import com.seibel.lod.core.file.renderfile.ILodRenderSourceProvider;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.util.DetailDistanceUtil;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.util.objects.quadTree.QuadNode;
import com.seibel.lod.core.util.objects.quadTree.QuadTree;
import org.apache.logging.log4j.Logger;
@@ -15,47 +16,17 @@ import java.util.Iterator;
/**
* This quadTree structure is our core data structure and holds
* all rendering data. <br><br>
*
* This class represent a circular quadTree of lodSections. <br>
* Each section at level n is populated in one or more ways: <br>
* -by constructing it from the data of all the children sections (lower levels) <br>
* -by loading from file <br>
* -by adding data with the lodBuilder <br>
* <br><br>
* The QuadTree is built from several layers of 2d ring buffers.
* all rendering data.
*/
public class LodQuadTree extends QuadTree<LodRenderSection> implements AutoCloseable
{
/**
* Note: all config values should be via the class that extends this class, and
* by implementing different abstract methods
*/
public static final byte TREE_LOWEST_DETAIL_LEVEL = ColumnRenderSource.SECTION_SIZE_OFFSET;
private static final boolean SUPER_VERBOSE_LOGGING = false;
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public final byte getLayerDataDetailOffset() { return ColumnRenderSource.SECTION_SIZE_OFFSET; }
public final byte getLayerDataDetail(byte sectionDetailLevel) { return (byte) (sectionDetailLevel - this.getLayerDataDetailOffset()); }
public final byte getLayerSectionDetailOffset() { return ColumnRenderSource.SECTION_SIZE_OFFSET; }
public final byte getLayerSectionDetail(byte dataDetail) { return (byte) (dataDetail + this.getLayerSectionDetailOffset()); }
public final int blockRenderDistance;
private final ILodRenderSourceProvider renderSourceProvider;
/** How many {@link LodRenderSection}'s are currently loading */
private int numberOfRenderSectionsLoading = 0;
/**
* Indicates how many {@link LodRenderSection}'s can load concurrently. <br>
* Prevents large number of {@link ILodRenderSourceProvider} tasks from building up when initially loading.
*/
private static final int MAX_NUMBER_OF_LOADING_RENDER_SECTIONS = 2;
private final IDhClientLevel level; //FIXME: Proper hierarchy to remove this reference!
@@ -76,73 +47,9 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements AutoClose
}
/**
* This method return the LodSection at the given detail level and level coordinate x and z
* @param detailLevel detail level of the section
* @param x x coordinate of the section
* @param z z coordinate of the section
* @return the LodSection
*/
public LodRenderSection getSection(byte detailLevel, int x, int z) { return this.getValue(new DhSectionPos(detailLevel, x, z)); }
public LodRenderSection getSection(DhSectionPos pos) { return this.getValue(pos); }
/**
* This method will compute the detail level based on player position and section pos
* Override this method if you want to use a different algorithm
* @param playerPos player position as a reference for calculating the detail level
* @param sectionPos section position
* @return detail level of this section pos
*/
public byte calculateExpectedDetailLevel(DhBlockPos2D playerPos, DhSectionPos sectionPos)
{
return DetailDistanceUtil.getDetailLevelFromDistance(playerPos.dist(sectionPos.getCenter().getCenterBlockPos()));
}
/**
* The method will return the highest detail level in a circle around the center
* Override this method if you want to use a different algorithm
* Note: the returned distance should always be the ceiling estimation of the distance
* //TODO: Make this input a bbox or a circle or something....
* @param distance the circle radius
* @return the highest detail level in the circle
*/
public byte getMaxDetailInRange(double distance) { return DetailDistanceUtil.getDetailLevelFromDistance(distance); }
/**
* The method will return the furthest distance to the center for the given detail level
* Override this method if you want to use a different algorithm
* Note: the returned distance should always be the ceiling estimation of the distance
* //TODO: Make this return a bbox instead of a distance in circle
* @param detailLevel detail level
* @return the furthest distance to the center, in blocks
*/
public int getFurthestDistance(byte detailLevel)
{
return (int)Math.ceil(DetailDistanceUtil.getDrawDistanceFromDetail(detailLevel + 1));
// +1 because that's the border to the next detail level, and we want to include up to it.
}
/**
* Given a section pos at level n this method returns the parent section at level n+1
* @param pos the section position
* @return the parent LodSection
*/
public LodRenderSection getParentSection(DhSectionPos pos) { return this.getSection(pos.getParentPos()); }
/**
* Given a section pos at level n and a child index this method return the
* child section at level n-1
* @param child0to3 since there are 4 possible children this index identify which one we are getting
* @return one of the child LodSection
*/
public LodRenderSection getChildSection(DhSectionPos pos, int child0to3) { return this.getSection(pos.getChildByIndex(child0to3)); }
// tick //
//=============//
// tick update //
//=============//
/**
* This function updates the quadTree based on the playerPos and the current game configs (static and global)
@@ -150,60 +57,81 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements AutoClose
*/
public void tick(DhBlockPos2D playerPos)
{
if (this.level == null)
{
// the level hasn't finished loading yet
// TODO sometimes null pointers still happen, when logging back into a world (maybe the old level isn't null but isn't valid either?)
return;
}
try
{
// recenter if necessary
// recenter if necessary, removing out of bounds sections
this.setCenterBlockPos(playerPos, LodRenderSection::disposeRenderData);
updateAllRenderSections(playerPos);
}
catch (Exception e)
{
// TODO when we are stable this shouldn't be necessary
LOGGER.error("Quad Tree tick exception for dimension: "+this.level.getClientLevelWrapper().getDimensionType().getDimensionName()+", exception: "+e.getMessage(), e);
}
}
private void updateAllRenderSections(DhBlockPos2D playerPos)
{
// make sure all root nodes are created
// walk through each root node
Iterator<DhSectionPos> rootPosIterator = this.rootNodePosIterator();
while (rootPosIterator.hasNext())
{
DhSectionPos rootSectionPos = rootPosIterator.next();
if (this.getNode(rootSectionPos) == null)
// make sure all root nodes have been created
DhSectionPos rootPos = rootPosIterator.next();
if (this.getNode(rootPos) == null)
{
LodRenderSection newRenderSection = new LodRenderSection(rootSectionPos);
this.setValue(rootSectionPos, newRenderSection);
this.setValue(rootPos, new LodRenderSection(rootPos));
}
}
// update all nodes in the tree
Iterator<DhSectionPos> rootNodeIterator = this.rootNodePosIterator();
while (rootNodeIterator.hasNext())
{
DhSectionPos rootPos = rootNodeIterator.next();
QuadNode<LodRenderSection> rootNode = this.getNode(rootPos); // should never be null
// iterate over nodes in this root
Iterator<QuadNode<LodRenderSection>> nodeIterator = rootNode.getNodeIterator();
while (nodeIterator.hasNext())
{
QuadNode<LodRenderSection> quadNode = nodeIterator.next();
recursivelyUpdateRenderSectionNode(playerPos, rootNode, quadNode, quadNode.sectionPos);
}
QuadNode<LodRenderSection> rootNode = this.getNode(rootPos);
recursivelyUpdateRenderSectionNode(playerPos, rootNode, rootNode, rootNode.sectionPos, false);
}
}
private void recursivelyUpdateRenderSectionNode(DhBlockPos2D playerPos, QuadNode<LodRenderSection> rootNode, QuadNode<LodRenderSection> nullableQuadNode, DhSectionPos sectionPos)
/** @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, DhSectionPos sectionPos, boolean parentRenderSectionIsEnabled)
{
LodRenderSection nullableRenderSection = null;
if (nullableQuadNode != null)
//===============================//
// node and render section setup //
//===============================//
// make sure the node is created
if (quadNode == null && this.isSectionPosInBounds(sectionPos)) // the position bounds should only fail when at the edge of the user's render distance
{
nullableRenderSection = nullableQuadNode.value;
rootNode.setValue(sectionPos, new LodRenderSection(sectionPos));
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;
}
// make sure the render section is created
LodRenderSection renderSection = quadNode.value;
// create a new render section if missing
if (renderSection == null)
{
LodRenderSection newRenderSection = new LodRenderSection(sectionPos);
rootNode.setValue(sectionPos, newRenderSection);
renderSection = newRenderSection;
}
//===============================//
// handle enabling, loading, //
// and disabling render sections //
//===============================//
// byte expectedDetailLevel = 6; // can be used instead of the following logic for testing
byte expectedDetailLevel = calculateExpectedDetailLevel(playerPos, sectionPos);
expectedDetailLevel += DhSectionPos.SECTION_BLOCK_DETAIL_LEVEL;
@@ -211,151 +139,129 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements AutoClose
if (sectionPos.sectionDetailLevel > expectedDetailLevel)
{
// section detail level too high...
// section detail level too high //
if (nullableRenderSection != null)
boolean isThisPositionBeingRendered = renderSection.isRenderingEnabled();
boolean allChildrenSectionsAreLoaded = true;
// recursively update all child render sections
Iterator<DhSectionPos> childPosIterator = quadNode.getChildPosIterator();
while (childPosIterator.hasNext())
{
if (areChildRenderSectionsEnabled(nullableRenderSection))
{
nullableRenderSection.disableAndDisposeRendering();
}
DhSectionPos childPos = childPosIterator.next();
QuadNode<LodRenderSection> childNode = rootNode.getNode(childPos);
boolean childSectionLoaded = this.recursivelyUpdateRenderSectionNode(playerPos, rootNode, childNode, childPos, isThisPositionBeingRendered || parentRenderSectionIsEnabled);
allChildrenSectionsAreLoaded = childSectionLoaded && allChildrenSectionsAreLoaded;
}
if (nullableQuadNode == null)
if (!allChildrenSectionsAreLoaded)
{
// ...create self
if (this.isSectionPosInBounds(sectionPos)) // this should only fail when at the edge of the user's render distance
{
rootNode.setValue(sectionPos, new LodRenderSection(sectionPos));
}
// not all child positions are loaded yet, or this section is out of render range
return isThisPositionBeingRendered;
}
else
{
Iterator<DhSectionPos> childPosIterator = nullableQuadNode.getChildPosIterator();
// all child positions are loaded, disable this section and enable the children.
renderSection.disposeRenderData();
renderSection.disableRendering();
// walk back down the tree and enable the child sections //TODO there are probably more efficient ways of doing this, but this will work for now
childPosIterator = quadNode.getChildPosIterator();
while (childPosIterator.hasNext())
{
DhSectionPos childPos = childPosIterator.next();
QuadNode<LodRenderSection> childNode = rootNode.getNode(childPos);
recursivelyUpdateRenderSectionNode(playerPos, rootNode, childNode, childPos);
boolean childSectionLoaded = this.recursivelyUpdateRenderSectionNode(playerPos, rootNode, childNode, childPos, parentRenderSectionIsEnabled);
allChildrenSectionsAreLoaded = childSectionLoaded && allChildrenSectionsAreLoaded;
}
LodUtil.assertTrue(allChildrenSectionsAreLoaded, "Potential QuadTree concurrency issue. All child sections should be enabled and ready to render.");
// this section is now being rendered via its children
return true;
}
}
// 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 (sectionPos.sectionDetailLevel == expectedDetailLevel || sectionPos.sectionDetailLevel == expectedDetailLevel-1)
{
// this is the correct detail level and should be rendered
// this is the detail level we want to render //
if (nullableQuadNode == null)
// prepare this section for rendering
renderSection.loadRenderSource(this.renderSourceProvider, this.level);
// wait for the parent to disable before enabling this section, so we don't overdraw/overlap render sections
if (!parentRenderSectionIsEnabled && renderSection.isRenderDataLoaded())
{
if (this.isSectionPosInBounds(sectionPos))
{
// create new value and update next tick
rootNode.setValue(sectionPos, new LodRenderSection(sectionPos));
}
nullableQuadNode = rootNode.getNode(sectionPos);
}
if (nullableQuadNode != null)
{
// create a new render section if missing
if (nullableRenderSection == null)
{
LodRenderSection newRenderSection = new LodRenderSection(sectionPos);
rootNode.setValue(sectionPos, newRenderSection);
nullableRenderSection = newRenderSection;
}
//if (!areParentRenderSectionsLoaded(sectionPos)) // TODO not functional yet
{
// enable the render section
nullableRenderSection.loadRenderSource(this.renderSourceProvider);
// determine if the section has loaded yet // TODO rename "tick" to check loading future or something?
nullableRenderSection.enableRendering(this.level);
}
renderSection.enableRendering();
// delete/disable children
if (isSectionLoaded(nullableRenderSection))
// delete/disable children, all of them will be a lower detail level than requested
quadNode.deleteAllChildren((childRenderSection) ->
{
nullableQuadNode.deleteAllChildren((renderSection) ->
if (childRenderSection != null)
{
if (renderSection != null)
{
renderSection.disableAndDisposeRendering();
}
});
}
childRenderSection.disposeRenderData();
childRenderSection.disableRendering();
}
});
}
}
}
/**
* Used to determine if a section can unload or not.
* If this returns true, that means there are child render sections ready to render,
* so there won't be any holes in the world by disabling the parent.
* <br><Br>
* FIXME sometimes sections will render on top of each other
*/
private boolean areChildRenderSectionsEnabled(LodRenderSection renderSection)
{
if (renderSection == null)
{
// this section isn't loaded
return false;
}
if (renderSection.pos.sectionDetailLevel == TREE_LOWEST_DETAIL_LEVEL)
{
// this section is at the bottom detail level and has no children
return isSectionEnabled(renderSection);
return renderSection.isRenderDataLoaded();
}
else
{
// recursively check if all children are loaded
for (int i = 0; i < 4; i++)
{
DhSectionPos childPos = renderSection.pos.getChildByIndex(i);
// if a section is out of bounds, act like it is loaded
if (this.isSectionPosInBounds(childPos))
{
LodRenderSection child = this.getChildSection(renderSection.pos, i);
// check if either this child or all of its children are loaded
boolean childLoaded = isSectionEnabled(child) || areChildRenderSectionsEnabled(child);
if (!childLoaded)
{
// at least one child isn't loaded
return false;
}
}
}
// all children are loaded
return true;
throw new IllegalStateException("LodQuadTree shouldn't be updating renderSections below the expected detail level: ["+expectedDetailLevel+"].");
}
}
private static boolean isSectionEnabled(LodRenderSection renderSection)
//====================//
// detail level logic //
//====================//
/**
* This method will compute the detail level based on player position and section pos
* Override this method if you want to use a different algorithm
* @param playerPos player position as a reference for calculating the detail level
* @param sectionPos section position
* @return detail level of this section pos
*/
public byte calculateExpectedDetailLevel(DhBlockPos2D playerPos, DhSectionPos sectionPos)
{
return isSectionLoaded(renderSection)
&& renderSection.isRenderingEnabled()
&& renderSection.renderBufferRef.get() != null
&& renderSection.renderBufferRef.get().areBuffersUploaded();
return DetailDistanceUtil.getDetailLevelFromDistance(playerPos.dist(sectionPos.getCenter().getCenterBlockPos()));
}
private static boolean isSectionLoaded(LodRenderSection renderSection)
/**
* The method will return the highest detail level in a circle around the center
* Override this method if you want to use a different algorithm
* Note: the returned distance should always be the ceiling estimation of the distance
* //TODO: Make this input a bbox or a circle or something....
* @param distance the circle radius
* @return the highest detail level in the circle
*/
public byte getMaxDetailInRange(double distance) { return DetailDistanceUtil.getDetailLevelFromDistance(distance); }
/**
* The method will return the furthest distance to the center for the given detail level
* Override this method if you want to use a different algorithm
* Note: the returned distance should always be the ceiling estimation of the distance
* //TODO: Make this return a bbox instead of a distance in circle
* @param detailLevel detail level
* @return the furthest distance to the center, in blocks
*/
public int getFurthestDistance(byte detailLevel)
{
return renderSection != null
&& renderSection.isLoaded()
&& renderSection.getRenderSource() != null
&& !renderSection.getRenderSource().isEmpty();
return (int)Math.ceil(DetailDistanceUtil.getDrawDistanceFromDetail(detailLevel + 1));
// +1 because that's the border to the next detail level, and we want to include up to it.
}
@@ -370,6 +276,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements AutoClose
*/
public void clearRenderDataCache()
{
// TODO this causes some (harmless) file errors when called
LOGGER.info("Clearing render cache...");
Iterator<QuadNode<LodRenderSection>> nodeIterator = this.nodeIterator();
@@ -395,7 +302,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements AutoClose
*/
public void reloadPos(DhSectionPos pos)
{
LodRenderSection renderSection = this.getSection(pos);
LodRenderSection renderSection = this.getValue(pos);
if (renderSection != null)
{
renderSection.reload(this.renderSourceProvider);
@@ -11,20 +11,25 @@ import org.apache.logging.log4j.Logger;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
/**
* A render section represents an area that could be rendered.
* For more information see {@link LodQuadTree}.
*/
public class LodRenderSection
{
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
public final DhSectionPos pos;
private CompletableFuture<ColumnRenderSource> loadFuture;
private boolean isRenderEnabled = false;
private ColumnRenderSource renderSource;
private ILodRenderSourceProvider renderSourceProvider = null;
/** a reference is used so the render buffer can be swapped to and from the buffer builder */
public final AtomicReference<ColumnRenderBuffer> renderBufferRef = new AtomicReference<>();
private boolean isRenderingEnabled = false;
private ILodRenderSourceProvider renderSourceProvider = null;
private CompletableFuture<ColumnRenderSource> renderSourceLoadFuture;
private ColumnRenderSource renderSource;
public LodRenderSection(DhSectionPos pos) { this.pos = pos; }
@@ -35,7 +40,17 @@ public class LodRenderSection
// rendering //
//===========//
public void loadRenderSource(ILodRenderSourceProvider renderDataProvider)
public void enableRendering() { this.isRenderingEnabled = true; }
public void disableRendering() { this.isRenderingEnabled = false; }
//=============//
// render data //
//=============//
/** does nothing if a render source is already loaded or in the process of loading */
public void loadRenderSource(ILodRenderSourceProvider renderDataProvider, IDhClientLevel level)
{
this.renderSourceProvider = renderDataProvider;
if (this.renderSourceProvider == null)
@@ -43,48 +58,26 @@ public class LodRenderSection
return;
}
if (this.renderSource == null && this.loadFuture == null)
if (this.renderSource == null && this.renderSourceLoadFuture == null)
{
this.loadFuture = this.renderSourceProvider.read(this.pos);
this.loadFuture.whenComplete((renderSource, ex) ->
this.renderSourceLoadFuture = this.renderSourceProvider.readAsync(this.pos);
this.renderSourceLoadFuture.whenComplete((renderSource, ex) ->
{
this.renderSource = renderSource;
this.loadFuture = null;
this.renderSourceLoadFuture = null;
if (this.renderSource != null)
{
this.renderSource.allowRendering(level);
}
});
}
}
public void enableRendering(IDhClientLevel level)
{
this.isRenderEnabled = true;
if (this.renderSource != null)
{
this.renderSource.enableRender(level);
}
}
public void disableAndDisposeRendering()
{
if (!this.isRenderEnabled)
{
return;
}
this.disposeRenderData();
this.isRenderEnabled = false;
}
//========================//
// render source provider //
//========================//
public void reload(ILodRenderSourceProvider renderDataProvider)
{
// don't accidentally enable rendering for a disabled section
if (!this.isRenderEnabled)
if (!this.isRenderingEnabled)
{
return;
}
@@ -92,10 +85,10 @@ public class LodRenderSection
this.renderSourceProvider = renderDataProvider;
if (this.loadFuture != null)
if (this.renderSourceLoadFuture != null)
{
this.loadFuture.cancel(true);
this.loadFuture = null;
this.renderSourceLoadFuture.cancel(true);
this.renderSourceLoadFuture = null;
}
if (this.renderSource != null)
@@ -104,15 +97,10 @@ public class LodRenderSection
this.renderSource = null;
}
this.loadFuture = this.renderSourceProvider.read(this.pos);
this.renderSourceLoadFuture = this.renderSourceProvider.readAsync(this.pos);
}
//================//
// update methods //
//================//
public void disposeRenderData()
{
if (this.renderSource != null)
@@ -128,28 +116,45 @@ public class LodRenderSection
this.renderBufferRef.set(null);
}
if (this.loadFuture != null)
if (this.renderSourceLoadFuture != null)
{
this.loadFuture.cancel(true);
this.loadFuture = null;
this.renderSourceLoadFuture.cancel(true);
this.renderSourceLoadFuture = null;
}
}
//========================//
// getters and properties //
//========================//
public boolean shouldRender() { return this.isLoaded() && this.isRenderEnabled; }
public boolean isRenderingEnabled() { return this.isRenderEnabled; }
public boolean isLoaded() { return this.renderSource != null; }
public boolean isLoading() { return this.loadFuture != null; }
public boolean isOutdated() { return this.renderSource != null && !this.renderSource.isValid(); }
/** @return true if this section is loaded and set to render */
public boolean shouldRender() { return this.isRenderingEnabled && this.isRenderDataLoaded(); }
/** This can return true before the render data is loaded */
public boolean isRenderingEnabled() { return this.isRenderingEnabled; }
public ColumnRenderSource getRenderSource() { return this.renderSource; }
public CompletableFuture<ColumnRenderSource> getRenderSourceLoadingFuture() { return this.loadFuture; }
public boolean isRenderDataLoaded()
{
return this.renderSource != null
&&
(
(
// if true; either this section represents empty chunks or un-generated chunks.
// Either way, there isn't any data to render, but this should be considered "loaded"
this.renderSource.isEmpty()
)
||
(
// check if the buffers have been loaded
this.renderBufferRef.get() != null
&& this.renderBufferRef.get().areBuffersUploaded()
)
);
}
//==============//
@@ -160,8 +165,8 @@ public class LodRenderSection
return "LodRenderSection{" +
"pos=" + this.pos +
", lodRenderSource=" + this.renderSource +
", loadFuture=" + this.loadFuture +
", isRenderEnabled=" + this.isRenderEnabled +
", loadFuture=" + this.renderSourceLoadFuture +
", isRenderEnabled=" + this.isRenderingEnabled +
'}';
}
@@ -6,7 +6,6 @@ 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.LodUtil;
import com.seibel.lod.core.util.math.Vec3f;
import com.seibel.lod.core.util.objects.SortedArraySet;
import com.seibel.lod.core.util.objects.quadTree.QuadNode;
@@ -24,14 +23,14 @@ public class RenderBufferHandler
private static final Logger LOGGER = DhLoggerBuilder.getLogger();
/** contains all relevant data */
public final LodQuadTree quadTree;
public final LodQuadTree lodQuadTree;
// 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 lodQuadTree) { this.quadTree = lodQuadTree; }
public RenderBufferHandler(LodQuadTree lodQuadTree) { this.lodQuadTree = lodQuadTree; }
@@ -138,7 +137,7 @@ public class RenderBufferHandler
// Build the sorted list
this.loadedNearToFarBuffers = new SortedArraySet<>((a, b) -> -farToNearComparator.compare(a, b)); // TODO is the comparator named wrong?
Iterator<QuadNode<LodRenderSection>> nodeIterator = this.quadTree.nodeIterator();
Iterator<QuadNode<LodRenderSection>> nodeIterator = this.lodQuadTree.nodeIterator();
while (nodeIterator.hasNext())
{
QuadNode<LodRenderSection> node = nodeIterator.next();
@@ -167,30 +166,20 @@ public class RenderBufferHandler
this.loadedNearToFarBuffers.forEach(loadedBuffer -> loadedBuffer.buffer.renderTransparent(renderContext));
}
public void update()
public void updateQuadTreeRenderSources()
{
Iterator<QuadNode<LodRenderSection>> nodeIterator = this.quadTree.nodeIterator();
Iterator<QuadNode<LodRenderSection>> nodeIterator = this.lodQuadTree.nodeIterator();
while (nodeIterator.hasNext())
{
LodRenderSection renderSection = nodeIterator.next().value;
if (renderSection != null)
{
ColumnRenderSource currentRenderSource = renderSection.getRenderSource();
// Update self's render buffer state
if (!renderSection.shouldRender())
ColumnRenderSource sectionRenderSource = renderSection.getRenderSource();
// if the render source is present, attempt to load it
if (sectionRenderSource != null)
{
//TODO: Does this really need to force the old buffer to not be rendered?
AbstractRenderBuffer previousRenderBuffer = renderSection.renderBufferRef.getAndSet(null);
if (previousRenderBuffer != null)
{
previousRenderBuffer.close();
}
}
else
{
LodUtil.assertTrue(currentRenderSource != null); // section.shouldRender() should have ensured this
currentRenderSource.trySwapInNewlyBuiltRenderBuffer(renderSection.getRenderSource(), renderSection.renderBufferRef);
// TODO why are we always trying to swap the buffers? shouldn't we only swap them when a new buffer has been built? we have a future object specifically for that in ColumnRenderSource
sectionRenderSource.trySwapInNewlyBuiltRenderBuffer(renderSection.getRenderSource(), renderSection.renderBufferRef);
}
}
}
@@ -198,7 +187,7 @@ public class RenderBufferHandler
public void close()
{
Iterator<QuadNode<LodRenderSection>> nodeIterator = this.quadTree.nodeIterator();
Iterator<QuadNode<LodRenderSection>> nodeIterator = this.lodQuadTree.nodeIterator();
while (nodeIterator.hasNext())
{
LodRenderSection renderSection = nodeIterator.next().value;