From eb5da6fa4ddf34b8dedf9f6a9a4f8e25087423a7 Mon Sep 17 00:00:00 2001 From: James Seibel Date: Mon, 17 Apr 2023 21:40:45 -0500 Subject: [PATCH] Fix RenderSection overlap and holes --- .../render/ColumnRenderSource.java | 14 +- .../bufferBuilding/ColumnRenderBuffer.java | 2 + .../renderfile/ILodRenderSourceProvider.java | 6 +- .../file/renderfile/RenderMetaDataFile.java | 7 +- .../renderfile/RenderSourceFileHandler.java | 196 +++++---- .../lod/core/level/AbstractDhClientLevel.java | 8 +- .../core/level/states/ClientRenderState.java | 2 +- .../seibel/lod/core/render/LodQuadTree.java | 377 +++++++----------- .../lod/core/render/LodRenderSection.java | 129 +++--- .../lod/core/render/RenderBufferHandler.java | 33 +- 10 files changed, 358 insertions(+), 416 deletions(-) diff --git a/core/src/main/java/com/seibel/lod/core/dataObjects/render/ColumnRenderSource.java b/core/src/main/java/com/seibel/lod/core/dataObjects/render/ColumnRenderSource.java index 376780d94..e0e14bef1 100644 --- a/core/src/main/java/com/seibel/lod/core/dataObjects/render/ColumnRenderSource.java +++ b/core/src/main/java/com/seibel/lod/core/dataObjects/render/ColumnRenderSource.java @@ -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); diff --git a/core/src/main/java/com/seibel/lod/core/dataObjects/render/bufferBuilding/ColumnRenderBuffer.java b/core/src/main/java/com/seibel/lod/core/dataObjects/render/bufferBuilding/ColumnRenderBuffer.java index 569cdfa79..fc9a40c93 100644 --- a/core/src/main/java/com/seibel/lod/core/dataObjects/render/bufferBuilding/ColumnRenderBuffer.java +++ b/core/src/main/java/com/seibel/lod/core/dataObjects/render/bufferBuilding/ColumnRenderBuffer.java @@ -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 diff --git a/core/src/main/java/com/seibel/lod/core/file/renderfile/ILodRenderSourceProvider.java b/core/src/main/java/com/seibel/lod/core/file/renderfile/ILodRenderSourceProvider.java index 60a25a1e0..3124b173b 100644 --- a/core/src/main/java/com/seibel/lod/core/file/renderfile/ILodRenderSourceProvider.java +++ b/core/src/main/java/com/seibel/lod/core/file/renderfile/ILodRenderSourceProvider.java @@ -16,10 +16,10 @@ import java.util.concurrent.CompletableFuture; */ public interface ILodRenderSourceProvider extends AutoCloseable { - CompletableFuture read(DhSectionPos pos); + CompletableFuture readAsync(DhSectionPos pos); void addScannedFile(Collection detectedFiles); - void write(DhSectionPos sectionPos, ChunkSizedFullDataSource chunkData); - CompletableFuture flushAndSave(); + void writeChunkDataToFile(DhSectionPos sectionPos, ChunkSizedFullDataSource chunkData); + CompletableFuture flushAndSaveAsync(); /** Returns true if the data was refreshed, false otherwise */ boolean refreshRenderSource(ColumnRenderSource source); diff --git a/core/src/main/java/com/seibel/lod/core/file/renderfile/RenderMetaDataFile.java b/core/src/main/java/com/seibel/lod/core/file/renderfile/RenderMetaDataFile.java index 2b0484a9d..4890d733b 100644 --- a/core/src/main/java/com/seibel/lod/core/file/renderfile/RenderMetaDataFile.java +++ b/core/src/main/java/com/seibel/lod/core/file/renderfile/RenderMetaDataFile.java @@ -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); diff --git a/core/src/main/java/com/seibel/lod/core/file/renderfile/RenderSourceFileHandler.java b/core/src/main/java/com/seibel/lod/core/file/renderfile/RenderSourceFileHandler.java index ec139c4ce..49abb9d52 100644 --- a/core/src/main/java/com/seibel/lod/core/file/renderfile/RenderSourceFileHandler.java +++ b/core/src/main/java/com/seibel/lod/core/file/renderfile/RenderSourceFileHandler.java @@ -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 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 newRenderFiles) { HashMultimap 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 read(DhSectionPos pos) + public CompletableFuture 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 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.
+ * 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 flushAndSave() + public CompletableFuture 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> 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 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 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 renderSourceReference = new WeakReference<>(renderSource); // TODO why is this a week reference? CompletableFuture fullDataSourceFuture = this.fullDataSourceProvider.read(renderSource.getSectionPos()); fullDataSourceFuture = fullDataSourceFuture.thenApply((fullDataSource) -> @@ -289,51 +291,59 @@ public class RenderSourceFileHandler implements ILodRenderSourceProvider return null; }); + + // future returned + CompletableFuture 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 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> 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);} + + } diff --git a/core/src/main/java/com/seibel/lod/core/level/AbstractDhClientLevel.java b/core/src/main/java/com/seibel/lod/core/level/AbstractDhClientLevel.java index 22eb37290..c678352de 100644 --- a/core/src/main/java/com/seibel/lod/core/level/AbstractDhClientLevel.java +++ b/core/src/main/java/com/seibel/lod/core/level/AbstractDhClientLevel.java @@ -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 { diff --git a/core/src/main/java/com/seibel/lod/core/level/states/ClientRenderState.java b/core/src/main/java/com/seibel/lod/core/level/states/ClientRenderState.java index 8fdff93c2..ede27a80b 100644 --- a/core/src/main/java/com/seibel/lod/core/level/states/ClientRenderState.java +++ b/core/src/main/java/com/seibel/lod/core/level/states/ClientRenderState.java @@ -52,7 +52,7 @@ public class ClientRenderState this.renderer.close(); this.quadtree.close(); - return this.renderSourceFileHandler.flushAndSave(); + return this.renderSourceFileHandler.flushAndSaveAsync(); } } diff --git a/core/src/main/java/com/seibel/lod/core/render/LodQuadTree.java b/core/src/main/java/com/seibel/lod/core/render/LodQuadTree.java index 1f6002804..a648bff52 100644 --- a/core/src/main/java/com/seibel/lod/core/render/LodQuadTree.java +++ b/core/src/main/java/com/seibel/lod/core/render/LodQuadTree.java @@ -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.

- * - * This class represent a circular quadTree of lodSections.
- * Each section at level n is populated in one or more ways:
- * -by constructing it from the data of all the children sections (lower levels)
- * -by loading from file
- * -by adding data with the lodBuilder
- *

- * The QuadTree is built from several layers of 2d ring buffers. + * all rendering data. */ public class LodQuadTree extends QuadTree 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.
- * 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 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 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 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 rootNodeIterator = this.rootNodePosIterator(); - while (rootNodeIterator.hasNext()) - { - DhSectionPos rootPos = rootNodeIterator.next(); - QuadNode rootNode = this.getNode(rootPos); // should never be null - // iterate over nodes in this root - Iterator> nodeIterator = rootNode.getNodeIterator(); - while (nodeIterator.hasNext()) - { - QuadNode quadNode = nodeIterator.next(); - recursivelyUpdateRenderSectionNode(playerPos, rootNode, quadNode, quadNode.sectionPos); - } + QuadNode rootNode = this.getNode(rootPos); + recursivelyUpdateRenderSectionNode(playerPos, rootNode, rootNode, rootNode.sectionPos, false); } } - private void recursivelyUpdateRenderSectionNode(DhBlockPos2D playerPos, QuadNode rootNode, QuadNode 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 rootNode, QuadNode 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 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 childPosIterator = quadNode.getChildPosIterator(); + while (childPosIterator.hasNext()) { - if (areChildRenderSectionsEnabled(nullableRenderSection)) - { - nullableRenderSection.disableAndDisposeRendering(); - } + DhSectionPos childPos = childPosIterator.next(); + QuadNode 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 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 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. - *

- * 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 implements AutoClose */ public void clearRenderDataCache() { + // TODO this causes some (harmless) file errors when called LOGGER.info("Clearing render cache..."); Iterator> nodeIterator = this.nodeIterator(); @@ -395,7 +302,7 @@ public class LodQuadTree extends QuadTree implements AutoClose */ public void reloadPos(DhSectionPos pos) { - LodRenderSection renderSection = this.getSection(pos); + LodRenderSection renderSection = this.getValue(pos); if (renderSection != null) { renderSection.reload(this.renderSourceProvider); diff --git a/core/src/main/java/com/seibel/lod/core/render/LodRenderSection.java b/core/src/main/java/com/seibel/lod/core/render/LodRenderSection.java index 09d97c526..f607a53b4 100644 --- a/core/src/main/java/com/seibel/lod/core/render/LodRenderSection.java +++ b/core/src/main/java/com/seibel/lod/core/render/LodRenderSection.java @@ -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 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 renderBufferRef = new AtomicReference<>(); + private boolean isRenderingEnabled = false; + + private ILodRenderSourceProvider renderSourceProvider = null; + private CompletableFuture 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 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 + '}'; } diff --git a/core/src/main/java/com/seibel/lod/core/render/RenderBufferHandler.java b/core/src/main/java/com/seibel/lod/core/render/RenderBufferHandler.java index 1ab58d137..0ae8ccc8b 100644 --- a/core/src/main/java/com/seibel/lod/core/render/RenderBufferHandler.java +++ b/core/src/main/java/com/seibel/lod/core/render/RenderBufferHandler.java @@ -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 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> nodeIterator = this.quadTree.nodeIterator(); + Iterator> nodeIterator = this.lodQuadTree.nodeIterator(); while (nodeIterator.hasNext()) { QuadNode 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> nodeIterator = this.quadTree.nodeIterator(); + Iterator> 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> nodeIterator = this.quadTree.nodeIterator(); + Iterator> nodeIterator = this.lodQuadTree.nodeIterator(); while (nodeIterator.hasNext()) { LodRenderSection renderSection = nodeIterator.next().value;