Refactor and cleanup render buffer management

This commit is contained in:
TomTheFurry
2023-06-15 21:55:18 +08:00
parent 0631f7adf7
commit 175581ca76
13 changed files with 326 additions and 511 deletions
@@ -22,7 +22,9 @@ import com.seibel.lod.core.util.LodUtil;
import org.apache.logging.log4j.Logger;
import java.io.*;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;
/**
@@ -62,20 +64,6 @@ public class ColumnRenderSource
private boolean isEmpty = true;
public EDhApiWorldGenerationStep worldGenStep;
private IDhClientLevel level = null; //FIXME: hack to pass level into tryBuildBuffer
//FIXME: Temp Hack to prevent swapping buffers too quickly
private long lastNs = -1;
/** 2 sec */
private static final long SWAP_TIMEOUT_IN_NS = 2_000000000L;
/** 1 sec */
private static final long SWAP_BUSY_COLLISION_TIMEOUT_IN_NS = 1_000000000L;
private CompletableFuture<ColumnRenderBuffer> buildRenderBufferFuture = null;
private final Reference<ColumnRenderBuffer> columnRenderBufferRef = new Reference<>();
//==============//
// constructors //
//==============//
@@ -342,94 +330,7 @@ public class ColumnRenderSource
public byte getDetailOffset() { return SECTION_SIZE_OFFSET; }
//================//
// Render Methods //
//================//
// TODO return future?
private void tryBuildBuffer(IDhClientLevel level, ColumnRenderSource[] adjacentRenderSources)
{
if (this.buildRenderBufferFuture == null && !ColumnRenderBufferBuilder.isBusy() && !this.isEmpty)
{
this.buildRenderBufferFuture = ColumnRenderBufferBuilder.buildBuffers(level, this.columnRenderBufferRef, this, adjacentRenderSources);
}
}
private void cancelBuildBuffer()
{
if (this.buildRenderBufferFuture != null)
{
//LOGGER.info("Cancelling build of render buffer for {}", sectionPos);
this.buildRenderBufferFuture.cancel(true);
this.buildRenderBufferFuture = null;
}
}
public void allowRendering(IDhClientLevel level) { this.level = level; }
public void disableRender() { this.cancelBuildBuffer(); }
public void dispose() { this.cancelBuildBuffer(); }
/**
* Try and swap in new render buffer for this section. Note that before this call, there should be no other
* places storing or referencing the render buffer.
* @param renderBufferRefToSwap The slot for swapping in the new buffer.
* @return True if the swap was successful. False if swap is not needed or if it is in progress.
*/
public boolean trySwapInNewlyBuiltRenderBuffer(AtomicReference<ColumnRenderBuffer> renderBufferRefToSwap, ColumnRenderSource[] adjacentRenderSources)
{
// prevent swapping the buffer to quickly
if (this.lastNs != -1 && System.nanoTime() - this.lastNs < SWAP_TIMEOUT_IN_NS)
{
return false;
}
if (this.buildRenderBufferFuture != null)
{
if (this.buildRenderBufferFuture.isDone())
{
this.lastNs = System.nanoTime();
//LOGGER.info("Swapping render buffer for {}", sectionPos);
ColumnRenderBuffer newBuffer = this.buildRenderBufferFuture.join();
LodUtil.assertTrue(newBuffer.buffersUploaded, "The buffer future for "+this.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.buffersUploaded = false;
}
ColumnRenderBuffer swapped = this.columnRenderBufferRef.swap(oldBuffer);
LodUtil.assertTrue(swapped == null);
this.buildRenderBufferFuture = null;
return true;
}
}
else
{
if (!this.isEmpty)
{
if (ColumnRenderBufferBuilder.isBusy())
{
this.lastNs += (long) (SWAP_BUSY_COLLISION_TIMEOUT_IN_NS * Math.random());
}
else
{
this.tryBuildBuffer(this.level, adjacentRenderSources);
}
}
}
return false;
}
public byte getRenderDataFormatVersion() { return DATA_FORMAT_VERSION; }
@@ -14,6 +14,7 @@ import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.pos.DhBlockPos;
import com.seibel.lod.core.render.glObject.GLProxy;
import com.seibel.lod.core.render.glObject.buffer.GLVertexBuffer;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.util.RenderDataPointUtil;
import com.seibel.lod.core.util.ThreadUtil;
import com.seibel.lod.core.util.objects.Reference;
@@ -90,71 +91,71 @@ public class ColumnRenderBufferBuilder
LOGGER.error("\"LodNodeBufferBuilder\" was unable to build quads: ", e3);
throw e3;
}
}, bufferBuilderThreadPool)
.thenApplyAsync((quadBuilder) ->
{
try
{
EVENT_LOGGER.trace("RenderRegion start Upload @ "+renderSource.sectionPos);
GLProxy glProxy = GLProxy.getInstance();
EGpuUploadMethod method = GLProxy.getInstance().getGpuUploadMethod();
EGLProxyContext oldContext = glProxy.getGlContext();
glProxy.setGlContext(EGLProxyContext.LOD_BUILDER);
ColumnRenderBuffer buffer = renderBufferRef.swap(null);
if (buffer == null)
{
buffer = new ColumnRenderBuffer(new DhBlockPos(renderSource.sectionPos.getCorner().getCornerBlockPos(), clientLevel.getMinY()), renderSource.sectionPos);
}
try
{
EVENT_LOGGER.trace("RenderRegion start Upload @ "+renderSource.sectionPos);
GLProxy glProxy = GLProxy.getInstance();
EGpuUploadMethod method = GLProxy.getInstance().getGpuUploadMethod();
EGLProxyContext oldContext = glProxy.getGlContext();
glProxy.setGlContext(EGLProxyContext.LOD_BUILDER);
ColumnRenderBuffer buffer = renderBufferRef.swap(null);
if (buffer == null)
{
buffer = new ColumnRenderBuffer(new DhBlockPos(renderSource.sectionPos.getCorner().getCornerBlockPos(), clientLevel.getMinY()), renderSource.sectionPos);
}
buffer.buffersUploaded = false;
try
{
buffer.uploadBuffer(quadBuilder, method);
EVENT_LOGGER.trace("RenderRegion end Upload @ "+renderSource.sectionPos);
return buffer;
}
catch (Exception e)
buffer.uploadBuffer(quadBuilder, method);
LodUtil.assertTrue(buffer.buffersUploaded);
EVENT_LOGGER.trace("RenderRegion end Upload @ "+renderSource.sectionPos);
return buffer;
}
catch (Exception e)
{
buffer.close();
throw e;
}
finally
{
glProxy.setGlContext(oldContext);
}
}
catch (InterruptedException e)
{
throw UncheckedInterruptedException.convert(e);
}
catch (Throwable e3)
{
LOGGER.error("\"LodNodeBufferBuilder\" was unable to upload buffer: ", e3);
throw e3;
}
}, bufferUploaderThreadPool)
.handle((columnRenderBuffer, ex) ->
{
//LOGGER.info("RenderRegion endBuild @ {}", renderSource.sectionPos);
if (ex != null)
{
LOGGER.warn("Buffer building failed: "+ex.getMessage(), ex);
if (!renderBufferRef.isEmpty())
{
ColumnRenderBuffer buffer = renderBufferRef.swap(null);
buffer.close();
throw e;
}
finally
{
glProxy.setGlContext(oldContext);
}
return null;
}
catch (InterruptedException e)
else
{
throw UncheckedInterruptedException.convert(e);
LodUtil.assertTrue(columnRenderBuffer.buffersUploaded);
return columnRenderBuffer;
}
catch (Throwable e3)
{
LOGGER.error("\"LodNodeBufferBuilder\" was unable to upload buffer: ", e3);
throw e3;
}
},
bufferUploaderThreadPool).handle((columnRenderBuffer, ex) ->
{
//LOGGER.info("RenderRegion endBuild @ {}", renderSource.sectionPos);
if (ex != null)
{
LOGGER.warn("Buffer building failed: "+ex.getMessage(), ex);
if (!renderBufferRef.isEmpty())
{
ColumnRenderBuffer buffer = renderBufferRef.swap(null);
buffer.close();
}
return null;
}
else
{
return columnRenderBuffer;
}
});
});
}
private static void makeLodRenderData(LodQuadBuilder quadBuilder, ColumnRenderSource renderSource, ColumnRenderSource[] adjRegions)
{
@@ -20,10 +20,7 @@ import org.apache.logging.log4j.Logger;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
@@ -314,7 +311,26 @@ public class FullDataFileHandler implements IFullDataSourceProvider
}
return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));
}
@Override
public CompletableFuture<Void> flushAndSave(DhSectionPos sectionPos)
{
FullDataMetaFile metaFile = this.fileBySectionPos.get(sectionPos);
if (metaFile == null)
{
return CompletableFuture.completedFuture(null);
}
return metaFile.flushAndSaveAsync();
}
private LinkedList<Consumer<IFullDataSource>> onUpdatedListeners = new LinkedList<>();
@Override
public synchronized void addOnUpdatedListener(Consumer<IFullDataSource> listener)
{
this.onUpdatedListeners.add(listener);
}
// @Override
// public long getCacheVersion(DhSectionPos sectionPos)
// {
@@ -402,7 +418,7 @@ public class FullDataFileHandler implements IFullDataSourceProvider
}
@Override
public IFullDataSource onDataFileLoaded(IFullDataSource source, BaseMetaData metaData,
public CompletableFuture<IFullDataSource> onDataFileLoaded(IFullDataSource source, BaseMetaData metaData,
Consumer<IFullDataSource> onUpdated, Function<IFullDataSource, Boolean> updater, boolean justCreated)
{
boolean changed = updater.apply(source);
@@ -422,7 +438,7 @@ public class FullDataFileHandler implements IFullDataSourceProvider
{
onUpdated.accept(source);
}
return source;
return CompletableFuture.completedFuture(source);
}
@Override
public CompletableFuture<IFullDataSource> onDataFileRefresh(IFullDataSource source, BaseMetaData metaData, Function<IFullDataSource, Boolean> updater, Consumer<IFullDataSource> onUpdated)
@@ -163,7 +163,7 @@ public class FullDataMetaFile extends AbstractMetaDataContainerFile
this.baseMetaData = this._makeBaseMetaData(fullDataSource);
return fullDataSource;
})
.thenApply((fullDataSource) -> this.fullDataSourceProvider.onDataFileLoaded(fullDataSource, this.baseMetaData, this::_updateAndWriteDataSource, this::_applyWriteQueueToFullDataSource, true))
.thenCompose((fullDataSource) -> this.fullDataSourceProvider.onDataFileLoaded(fullDataSource, this.baseMetaData, this::_updateAndWriteDataSource, this::_applyWriteQueueToFullDataSource, true))
.whenComplete((fullDataSource, exception) ->
{
if (exception != null)
@@ -223,12 +223,10 @@ public class FullDataMetaFile extends AbstractMetaDataContainerFile
LodUtil.assertTrue(this.inCacheWriteAccessFuture.get() == null,
"No one should be writing to the cache while we are in the process of " +
"loading one into the cache! Is this a deadlock?");
// fire the onDataLoaded method
fullDataSource = this.fullDataSourceProvider.onDataFileLoaded(fullDataSource, this.baseMetaData, this::_updateAndWriteDataSource, this::_applyWriteQueueToFullDataSource, false);
return fullDataSource;
}, executorService)
.thenCompose((fullDataSource) -> this.fullDataSourceProvider.onDataFileLoaded(fullDataSource, this.baseMetaData, this::_updateAndWriteDataSource, this::_applyWriteQueueToFullDataSource, false))
.exceptionally((ex) ->
{
if (ex instanceof InterruptedException)
@@ -243,11 +241,8 @@ public class FullDataMetaFile extends AbstractMetaDataContainerFile
//LOGGER.warn(FullDataMetaFile.class.getSimpleName()+" loadOrGetCachedAsync attempted to use a closed thread pool.");
return null;
}
LOGGER.error("Error loading file "+this.file+": ", ex);
this.cachedFullDataSource = new SoftReference<>(null);
future.completeExceptionally(ex);
return null; // the return value here doesn't matter
})
@@ -207,115 +207,44 @@ public class GeneratedFullDataFileHandler extends FullDataFileHandler
public CompletableFuture<IFullDataSource> onCreateDataFile(FullDataMetaFile file)
{
return this.spawnGenTasks(file, null);
/* DhSectionPos pos = file.pos;
ArrayList<FullDataMetaFile> existingFiles = new ArrayList<>();
ArrayList<DhSectionPos> missingPositions = new ArrayList<>();
this.getDataFilesForPosition(pos, pos, existingFiles, missingPositions);
// confirm the quad tree has at least one node in it
LodUtil.assertTrue(!missingPositions.isEmpty() || !existingFiles.isEmpty());
// determine the type of dataSource that should be used for this position
IIncompleteFullDataSource incompleteFullDataSource;
if (pos.sectionDetailLevel <= HighDetailIncompleteFullDataSource.MAX_SECTION_DETAIL)
{
incompleteFullDataSource = HighDetailIncompleteFullDataSource.createEmpty(pos);
}
else
{
incompleteFullDataSource = LowDetailIncompleteFullDataSource.createEmpty(pos);
}
if (missingPositions.size() == 1 && existingFiles.isEmpty() && missingPositions.get(0).equals(pos))
{
// No LOD data exists for this position yet
WorldGenerationQueue worldGenQueue = this.worldGenQueueRef.get();
if (worldGenQueue != null)
{
this.incompleteSourceGenRequests.add(pos);
// queue this section to be generated
GenTask genTask = new GenTask(pos, new WeakReference<>(incompleteFullDataSource));
worldGenQueue.submitGenTask(new DhLodPos(pos), incompleteFullDataSource.getDataDetailLevel(), genTask)
.whenComplete((genTaskResult, ex) ->
{
this.onWorldGenTaskComplete(genTaskResult, ex, genTask, pos);
this.fireOnGenPosSuccessListeners(pos);
this.incompleteSourceGenRequests.remove(pos);
});
}
// return the empty dataSource (it will be populated later)
return CompletableFuture.completedFuture(incompleteFullDataSource);
}
else
{
// LOD data exists for this position
// create the missing metaData files
for (DhSectionPos missingPos : missingPositions)
{
FullDataMetaFile newFile = this.getOrMakeFile(missingPos);
if (newFile != null)
{
existingFiles.add(newFile);
}
}
// LOGGER.debug("Creating "+pos+" from sampling "+existingFiles.size()+" files: "+existingFiles);
// read in the existing data
final ArrayList<CompletableFuture<Void>> loadDataFutures = new ArrayList<>(existingFiles.size());
for (FullDataMetaFile existingFile : existingFiles)
{
loadDataFutures.add(existingFile.loadOrGetCachedDataSourceAsync()
.exceptionally((ex) -> *//*Ignore file read errors*//*null)
.thenAccept((fullDataSource) ->
{
if (fullDataSource == null)
{
return;
}
this.checkIfSectionNeedsAdditionalGeneration(pos, fullDataSource);
//LOGGER.info("Merging data from {} into {}", data.getSectionPos(), pos);
incompleteFullDataSource.sampleFrom(fullDataSource);
})
);
}
return CompletableFuture.allOf(loadDataFutures.toArray(new CompletableFuture[0]))
.thenApply((voidValue) -> incompleteFullDataSource.tryPromotingToCompleteDataSource());
}*/
}
@Override
public IFullDataSource onDataFileLoaded(IFullDataSource source, BaseMetaData metaData,
public CompletableFuture<IFullDataSource> onDataFileLoaded(IFullDataSource source, BaseMetaData metaData,
Consumer<IFullDataSource> onUpdated, Function<IFullDataSource, Boolean> updater, boolean justCreated)
{
IFullDataSource fullDataSource = super.onDataFileLoaded(source, metaData, onUpdated, updater, justCreated);
if (fullDataSource instanceof CompleteFullDataSource || justCreated) {
return fullDataSource;
boolean changed = updater.apply(source);
if (source instanceof IIncompleteFullDataSource)
{
IFullDataSource newSource = ((IIncompleteFullDataSource) source).tryPromotingToCompleteDataSource();
changed |= newSource != source;
source = newSource;
}
this.spawnGenTasks(getOrMakeFile(metaData.pos), (IIncompleteFullDataSource)fullDataSource);
//checkIfSectionNeedsAdditionalGeneration(metaData.pos, fullDataSource);
return fullDataSource;
if (source instanceof IIncompleteFullDataSource && !justCreated) {
return this.spawnGenTasks(getOrMakeFile(metaData.pos), (IIncompleteFullDataSource)source)
.thenApply((newSource) -> {
onUpdated.accept(newSource);
return newSource;
});
}
else {
if (changed)
{
onUpdated.accept(source);
}
return CompletableFuture.completedFuture(source);
}
}
/* @Override
@Override
public CompletableFuture<IFullDataSource> onDataFileRefresh(IFullDataSource source, BaseMetaData metaData, Function<IFullDataSource, Boolean> updater, Consumer<IFullDataSource> onUpdated)
{
return super.onDataFileRefresh(source, metaData, updater, (IFullDataSource d) -> {
if (d instanceof CompleteFullDataSource) {
}
}
}*/
this.fireOnGenPosSuccessListeners(source.getSectionPos());
onUpdated.accept(d);
});
}
/**
* Checks if the given {@link IFullDataSource} is fully generated and
@@ -417,12 +346,14 @@ public class GeneratedFullDataFileHandler extends FullDataFileHandler
else if (genTaskResult.success)
{
// generation completed, update the files and listener(s)
this.fireOnGenPosSuccessListeners(pos);
this.flushAndSave(pos);
//this.fireOnGenPosSuccessListeners(pos);
return;
}
else
{
// generation didn't complete
LOGGER.error("Gen Task Failed at " + pos + ": " + genTaskResult);
}
@@ -439,7 +370,7 @@ public class GeneratedFullDataFileHandler extends FullDataFileHandler
{
//LOGGER.info("gen task completed for pos: ["+pos+"].");
// save the full data source
/* // save the full data source
FullDataMetaFile file = this.fileBySectionPos.get(pos);
if (file != null)
{
@@ -459,8 +390,7 @@ public class GeneratedFullDataFileHandler extends FullDataFileHandler
// doesn't appear to cause issues, but probably isn't desired either
//LOGGER.warn("fireOnGenPosSuccessListeners file null for pos: ["+pos+"].");
}
*/
// fire the event listeners
for (IOnWorldGenCompleteListener listener : this.onWorldGenTaskCompleteListeners)
{
@@ -501,7 +431,7 @@ public class GeneratedFullDataFileHandler extends FullDataFileHandler
}
@Override
public Consumer<ChunkSizedFullDataAccessor> getOnGenTaskCompleteConsumer()
public Consumer<ChunkSizedFullDataAccessor> getChunkDataConsumer()
{
if (this.loadedTargetFullDataSource == null)
{
@@ -521,7 +451,7 @@ public class GeneratedFullDataFileHandler extends FullDataFileHandler
}
};
}
public void releaseStrongReference() { this.loadedTargetFullDataSource = null; }
}
@@ -20,12 +20,15 @@ public interface IFullDataSourceProvider extends AutoCloseable
CompletableFuture<IFullDataSource> read(DhSectionPos pos);
void write(DhSectionPos sectionPos, ChunkSizedFullDataAccessor chunkData);
CompletableFuture<Void> flushAndSave();
CompletableFuture<Void> flushAndSave(DhSectionPos sectionPos);
void addOnUpdatedListener(Consumer<IFullDataSource> listener);
//long getCacheVersion(DhSectionPos sectionPos);
//boolean isCacheVersionValid(DhSectionPos sectionPos, long cacheVersion);
CompletableFuture<IFullDataSource> onCreateDataFile(FullDataMetaFile file);
IFullDataSource onDataFileLoaded(IFullDataSource source, BaseMetaData metaData, Consumer<IFullDataSource> onUpdated, Function<IFullDataSource, Boolean> updater, boolean justCreated);
CompletableFuture<IFullDataSource> onDataFileLoaded(IFullDataSource source, BaseMetaData metaData, Consumer<IFullDataSource> onUpdated, Function<IFullDataSource, Boolean> updater, boolean justCreated);
CompletableFuture<IFullDataSource> onDataFileRefresh(IFullDataSource source, BaseMetaData metaData, Function<IFullDataSource, Boolean> updater, Consumer<IFullDataSource> onUpdated);
File computeDataFilePath(DhSectionPos pos);
ExecutorService getIOExecutor();
@@ -374,7 +374,7 @@ public class WorldGenerationQueue implements Closeable, IDebugRenderable
// temporary solution to prevent generating the same section multiple times
LOGGER.warn("Duplicate generation section " + taskPos + " with granularity [" + granularity + "] at " + chunkPosMin + ". Skipping...");
StackTraceElement[] stackTrace = this.alreadyGeneratedPosHashSet.get(inProgressTaskGroup.group.pos);
//StackTraceElement[] stackTrace = this.alreadyGeneratedPosHashSet.get(inProgressTaskGroup.group.pos);
// sending a success result is necessary to make sure the render sections are reloaded correctly
inProgressTaskGroup.group.worldGenTasks.forEach(worldGenTask -> worldGenTask.future.complete(WorldGenResult.CreateSuccess(new DhSectionPos(granularity, taskPos))));
@@ -394,7 +394,7 @@ public class WorldGenerationQueue implements Closeable, IDebugRenderable
//LOGGER.info("Generating section "+taskPos+" with granularity "+granularity+" at "+chunkPosMin);
this.numberOfTasksQueued++;
inProgressTaskGroup.genFuture = this.startGenerationEvent(chunkPosMin, granularity, taskDetailLevel, inProgressTaskGroup.group::onGenerationComplete);
inProgressTaskGroup.genFuture = this.startGenerationEvent(chunkPosMin, granularity, taskDetailLevel, inProgressTaskGroup.group::consumeChunkData);
inProgressTaskGroup.genFuture.whenComplete((voidObj, exception) ->
{
this.numberOfTasksQueued--;
@@ -425,7 +425,7 @@ public class WorldGenerationQueue implements Closeable, IDebugRenderable
private CompletableFuture<Void> startGenerationEvent(
DhChunkPos chunkPosMin,
byte granularity, byte targetDataDetail,
Consumer<ChunkSizedFullDataAccessor> generationCompleteConsumer)
Consumer<ChunkSizedFullDataAccessor> chunkDataConsumer)
{
EDhApiDistantGeneratorMode generatorMode = Config.Client.Advanced.WorldGenerator.distantGeneratorMode.get();
return this.generator.generateChunks(chunkPosMin.x, chunkPosMin.z, granularity, targetDataDetail, generatorMode, worldGeneratorThreadPool, (generatedObjectArray) ->
@@ -433,7 +433,7 @@ public class WorldGenerationQueue implements Closeable, IDebugRenderable
try
{
IChunkWrapper chunk = SingletonInjector.INSTANCE.get(IWrapperFactory.class).createChunkWrapper(generatedObjectArray);
generationCompleteConsumer.accept(LodDataBuilder.createChunkData(chunk));
chunkDataConsumer.accept(LodDataBuilder.createChunkData(chunk));
}
catch (ClassCastException e)
{
@@ -13,6 +13,5 @@ public interface IWorldGenTaskTracker
/** Returns true if the task hasn't been garbage collected. */
boolean isMemoryAddressValid();
Consumer<ChunkSizedFullDataAccessor> getOnGenTaskCompleteConsumer();
Consumer<ChunkSizedFullDataAccessor> getChunkDataConsumer();
}
@@ -1,57 +0,0 @@
package com.seibel.lod.core.generation.tasks;
import com.seibel.lod.core.dataObjects.fullData.accessor.ChunkSizedFullDataAccessor;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
import com.seibel.lod.core.generation.WorldGenerationQueue;
/**
* Used to synchronize {@link WorldGenerationQueue} {@link WorldGenTask}'s
* if the {@link WorldGenTask} needs to be split up.
*
* @author Leetom
* @version 2022-11-25
*/
public class SplitWorldGenTaskTracker implements IWorldGenTaskTracker
{
public final IWorldGenTaskTracker parentTracker;
public final CompletableFuture<Boolean> parentFuture;
/** cached value to allow for quicker checking */
public boolean isValid = true;
public SplitWorldGenTaskTracker(IWorldGenTaskTracker parentTracker, CompletableFuture<Boolean> parentFuture)
{
this.parentTracker = parentTracker;
this.parentFuture = parentFuture;
}
/** Recalculates and returns the new {@link SplitWorldGenTaskTracker#isValid} value */
public boolean recalculateIsValid()
{
if (!this.isValid)
{
return false;
}
this.isValid = this.parentTracker.isMemoryAddressValid();
if (!this.isValid)
{
this.parentFuture.complete(false);
}
return this.isValid;
}
@Override
public boolean isMemoryAddressValid() { return this.isValid; }
@Override
public Consumer<ChunkSizedFullDataAccessor> getOnGenTaskCompleteConsumer() { return this.parentTracker.getOnGenTaskCompleteConsumer(); }
}
@@ -25,27 +25,23 @@ public final class WorldGenTaskGroup
this.pos = pos;
this.dataDetail = dataDetail;
}
public void onGenerationComplete(ChunkSizedFullDataAccessor chunkSizedFullDataView)
public void consumeChunkData(ChunkSizedFullDataAccessor chunkSizedFullDataView)
{
Iterator<WorldGenTask> tasks = this.worldGenTasks.iterator();
while (tasks.hasNext())
{
WorldGenTask task = tasks.next();
Consumer<ChunkSizedFullDataAccessor> onGenTaskCompleteConsumer = task.taskTracker.getOnGenTaskCompleteConsumer();
if (onGenTaskCompleteConsumer == null)
Consumer<ChunkSizedFullDataAccessor> chunkDataConsumer = task.taskTracker.getChunkDataConsumer();
if (chunkDataConsumer == null)
{
tasks.remove();
task.future.complete(WorldGenResult.CreateFail());
}
else
{
// TODO why aren't we removing the task if it has a consumer?
onGenTaskCompleteConsumer.accept(chunkSizedFullDataView);
chunkDataConsumer.accept(chunkSizedFullDataView);
}
}
}
}
@@ -198,9 +198,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements AutoClose
if (sectionPos.sectionDetailLevel > expectedDetailLevel)
{
// section detail level too high //
boolean isThisPositionBeingRendered = renderSection.isRenderingEnabled();
boolean canThisPosRender = renderSection.isRenderingEnabled();
boolean allChildrenSectionsAreLoaded = true;
// recursively update all child render sections
@@ -210,24 +208,20 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements AutoClose
DhSectionPos childPos = childPosIterator.next();
QuadNode<LodRenderSection> childNode = rootNode.getNode(childPos);
boolean childSectionLoaded = this.recursivelyUpdateRenderSectionNode(playerPos, rootNode, childNode, childPos, isThisPositionBeingRendered || parentRenderSectionIsEnabled);
boolean childSectionLoaded = this.recursivelyUpdateRenderSectionNode(playerPos, rootNode, childNode, childPos, canThisPosRender || parentRenderSectionIsEnabled);
allChildrenSectionsAreLoaded = childSectionLoaded && allChildrenSectionsAreLoaded;
}
if (!allChildrenSectionsAreLoaded)
{
// not all child positions are loaded yet, or this section is out of render range
return isThisPositionBeingRendered;
return canThisPosRender;
}
else
{
// all child positions are loaded, disable this section and enable its children.
renderSection.disposeRenderData();
renderSection.disableRendering();
renderSection.disposeRenderData();
// 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();
@@ -243,10 +237,9 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements AutoClose
{
// FIXME having world generation enabled in a pre-generated world that doesn't have any DH data can cause this to happen
// surprisingly reloadPos() doesn't appear to be the culprit, maybe there is an issue with reloading/changing the full data source?
LOGGER.debug("Potential QuadTree concurrency issue. All child sections should be enabled and ready to render for pos: "+sectionPos);
LOGGER.warn("Potential QuadTree concurrency issue. All child sections should be enabled and ready to render for pos: "+sectionPos);
}
// this section is now being rendered via its children
return true;
}
@@ -255,29 +248,25 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements AutoClose
else if (sectionPos.sectionDetailLevel == expectedDetailLevel || sectionPos.sectionDetailLevel == expectedDetailLevel-1)
{
// this is the detail level we want to render //
// 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 (!parentRenderSectionIsEnabled && renderSection.canRenderNow())
{
renderSection.enableRendering();
// delete/disable children, all of them will be a lower detail level than requested
quadNode.deleteAllChildren((childRenderSection) ->
{
if (childRenderSection != null)
{
childRenderSection.disposeRenderData();
childRenderSection.disableRendering();
childRenderSection.disposeRenderData();
}
});
}
return renderSection.isRenderDataLoaded();
return renderSection.canRenderNow();
}
else
{
@@ -2,17 +2,25 @@ package com.seibel.lod.core.render;
import com.seibel.lod.core.dataObjects.render.ColumnRenderSource;
import com.seibel.lod.core.dataObjects.render.bufferBuilding.ColumnRenderBuffer;
import com.seibel.lod.core.dataObjects.render.bufferBuilding.ColumnRenderBufferBuilder;
import com.seibel.lod.core.enums.ELodDirection;
import com.seibel.lod.core.level.IDhClientLevel;
import com.seibel.lod.core.logging.DhLoggerBuilder;
import com.seibel.lod.core.pos.DhSectionPos;
import com.seibel.lod.core.file.renderfile.ILodRenderSourceProvider;
import com.seibel.lod.core.render.renderer.DebugRenderer;
import com.seibel.lod.core.render.renderer.IDebugRenderable;
import com.seibel.lod.core.util.LodUtil;
import com.seibel.lod.core.util.objects.Reference;
import com.seibel.lod.core.util.objects.quadTree.QuadTree;
import org.apache.logging.log4j.Logger;
import java.awt.*;
import java.util.Objects;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;
/**
@@ -25,8 +33,6 @@ public class LodRenderSection implements IDebugRenderable
public final DhSectionPos pos;
/** 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;
/**
@@ -34,12 +40,25 @@ public class LodRenderSection implements IDebugRenderable
* a {@link ILodRenderSourceProvider} was already being loaded.
*/
private boolean reloadRenderSourceOnceLoaded = false;
private ILodRenderSourceProvider renderSourceProvider = null;
private CompletableFuture<ColumnRenderSource> renderSourceLoadFuture;
private ColumnRenderSource renderSource;
private IDhClientLevel level = null;
//FIXME: Temp Hack to prevent swapping buffers too quickly
private long lastNs = -1;
/** 2 sec */
private static final long SWAP_TIMEOUT_IN_NS = 2_000000000L;
/** 1 sec */
private static final long SWAP_BUSY_COLLISION_TIMEOUT_IN_NS = 1_000000000L;
private CompletableFuture<ColumnRenderBuffer> buildRenderBufferFuture = null;
private final Reference<ColumnRenderBuffer> inactiveRenderBufferRef = new Reference<>();
/** a reference is used so the render buffer can be swapped to and from the buffer builder */
public final AtomicReference<ColumnRenderBuffer> activeRenderBufferRef = new AtomicReference<>();
public LodRenderSection(DhSectionPos pos) {
@@ -58,13 +77,14 @@ public class LodRenderSection implements IDebugRenderable
if (renderSource != null) {
color = Color.blue;
if (isRenderingEnabled) color = Color.cyan;
if (isRenderingEnabled && isRenderDataLoaded()) color = Color.green;
if (buildRenderBufferFuture != null) color = Color.magenta;
if (canRenderNow()) color = Color.cyan;
if (canRenderNow() && isRenderingEnabled) color = Color.green;
}
float yOffset = Objects.hashCode(this) / (float) Integer.MAX_VALUE * 16f;
float yOffset = Objects.hashCode(this) / (float) Integer.MAX_VALUE * 16f + 16f;
//r.renderBox(this.pos, yOffset, yOffset, 0.1f, color);
r.renderBox(this.pos, yOffset, yOffset, 0.1f, color);
}
@@ -73,44 +93,23 @@ public class LodRenderSection implements IDebugRenderable
// rendering //
//===========//
public void enableRendering() { this.isRenderingEnabled = true; }
public void disableRendering() { this.isRenderingEnabled = false; }
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)
{
return;
}
// don't re-load or double load the render source
if (this.renderSource != null || this.renderSourceLoadFuture != null)
{
return;
}
private void startLoadRenderSource() {
this.renderSourceLoadFuture = this.renderSourceProvider.readAsync(this.pos);
this.renderSourceLoadFuture.whenComplete((renderSource, ex) ->
{
this.renderSourceLoadFuture = null;
this.renderSource = renderSource;
if (this.renderSource != null)
{
this.renderSource.allowRendering(level);
}
if (this.reloadRenderSourceOnceLoaded)
{
this.reloadRenderSourceOnceLoaded = false;
@@ -119,6 +118,23 @@ public class LodRenderSection implements IDebugRenderable
});
}
/** 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;
this.level = level;
if (this.renderSourceProvider == null)
{
return;
}
// don't re-load or double load the render source
if (this.renderSource != null || this.renderSourceLoadFuture != null)
{
return;
}
startLoadRenderSource();
}
public void reload(ILodRenderSourceProvider renderDataProvider)
{
this.renderSourceProvider = renderDataProvider;
@@ -126,85 +142,34 @@ public class LodRenderSection implements IDebugRenderable
{
return;
}
// don't accidentally enable rendering for a disabled section
if (!this.isRenderingEnabled)
{
return;
}
// wait for the current load future to finish before re-loading
if (this.renderSourceLoadFuture != null)
{
reloadRenderSourceOnceLoaded = true;
return;
}
this.renderSourceLoadFuture = this.renderSourceProvider.readAsync(this.pos);
this.renderSourceLoadFuture.whenComplete((renderSource, ex) ->
{
this.renderSourceLoadFuture = null;
// swap in the new render source
ColumnRenderSource oldRenderSource = this.renderSource;
this.renderSource = renderSource;
if (oldRenderSource != null)
{
// only clear the render source after we have a new one, otherwise flickering can happen
oldRenderSource.dispose();
}
if (this.reloadRenderSourceOnceLoaded)
{
this.reloadRenderSourceOnceLoaded = false;
reload(this.renderSourceProvider);
}
});
startLoadRenderSource();
}
public void disposeRenderData()
{
if (this.renderSource != null)
{
this.renderSource.disableRender();
this.renderSource.dispose();
this.renderSource = null;
}
if (this.renderBufferRef.get() != null)
{
this.renderBufferRef.get().close();
this.renderBufferRef.set(null);
}
if (this.renderSourceLoadFuture != null)
{
this.renderSourceLoadFuture.cancel(true);
this.renderSourceLoadFuture = null;
}
this.renderSourceProvider = null;
}
//========================//
// getters and properties //
//========================//
/** @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 boolean isRenderDataLoaded()
public boolean canRenderNow()
{
return this.renderSource != null
&&
@@ -217,14 +182,98 @@ public class LodRenderSection implements IDebugRenderable
||
(
// check if the buffers have been loaded
this.renderBufferRef.get() != null
&& this.renderBufferRef.get().buffersUploaded
this.activeRenderBufferRef.get() != null
)
);
}
//================//
// Render Methods //
//================//
private void cancelBuildBuffer()
{
if (this.buildRenderBufferFuture != null)
{
//LOGGER.info("Cancelling build of render buffer for {}", sectionPos);
this.buildRenderBufferFuture.cancel(true);
this.buildRenderBufferFuture = null;
}
}
private boolean isInBuildBufferTimeout() {
if (this.lastNs == -1) return false;
return true;
//boolean inTimeout = System.nanoTime() - this.lastNs < SWAP_TIMEOUT_IN_NS;
//if (!inTimeout && ColumnRenderBufferBuilder.isBusy()) {
// this.lastNs += (long) (SWAP_BUSY_COLLISION_TIMEOUT_IN_NS * Math.random());
// inTimeout = true;
//}
//return inTimeout;
}
/** @return true if this section is loaded and set to render */
public boolean canBuildBuffer() { return this.renderSource != null && this.buildRenderBufferFuture == null && !isInBuildBufferTimeout() && !this.renderSource.isEmpty(); }
/** @return true if this section is loaded and set to render */
public boolean canSwapBuffer() { return this.buildRenderBufferFuture != null && this.buildRenderBufferFuture.isDone(); }
/**
* Try and swap in new render buffer for this section. Note that before this call, there should be no other
* places storing or referencing the render buffer.
* @return True if the swap was successful. False if swap is not needed or if it is in progress.
*/
public boolean tryBuildAndSwapBuffer(QuadTree<LodRenderSection> tree)
{
boolean didSwapped = false;
if (canBuildBuffer()) {
ColumnRenderSource[] adjacentSources = new ColumnRenderSource[ELodDirection.ADJ_DIRECTIONS.length];
for (ELodDirection direction : ELodDirection.ADJ_DIRECTIONS) {
try {
DhSectionPos adjPos = pos.getAdjacentPos(direction);
LodRenderSection adjRenderSection = tree.getValue(adjPos);
// adjacent render sources can be null
if (adjRenderSection != null) {
adjacentSources[direction.ordinal() - 2] = adjRenderSection.renderSource; // can be null
}
} catch (IndexOutOfBoundsException e) {
// adjacent positions can be out of bounds, in that case a null render source will be used
}
}
this.buildRenderBufferFuture =
ColumnRenderBufferBuilder.buildBuffers(level, this.inactiveRenderBufferRef, renderSource, adjacentSources);
}
if (canSwapBuffer()) {
this.lastNs = System.nanoTime();
ColumnRenderBuffer newBuffer;
try {
newBuffer = this.buildRenderBufferFuture.getNow(null);
LodUtil.assertTrue(newBuffer != null && newBuffer.buffersUploaded, "The buffer future for "+pos+" returned an un-built buffer.");
this.buildRenderBufferFuture = null;
ColumnRenderBuffer oldBuffer = this.activeRenderBufferRef.getAndSet(newBuffer);
if (oldBuffer != null)
{
// the old buffer is now considered unloaded, it will need to be freshly re-loaded
oldBuffer.buffersUploaded = false;
}
ColumnRenderBuffer swapped = this.inactiveRenderBufferRef.swap(oldBuffer);
didSwapped = true;
LodUtil.assertTrue(swapped == null);
}
catch (CancellationException e1) {
// ignore.
this.buildRenderBufferFuture = null;
}
catch (CompletionException e) {
LOGGER.error("Unable to get render buffer for "+pos+".", e);
this.buildRenderBufferFuture = null;
}
}
return didSwapped;
}
//==============//
// base methods //
//==============//
@@ -239,7 +288,28 @@ public class LodRenderSection implements IDebugRenderable
}
public void dispose() {
DebugRenderer.unregister(this);
disposeRenderData();
DebugRenderer.unregister(this);
}
public void disposeRenderData()
{
disposeRenderBuffer();
if (this.renderSourceLoadFuture != null)
{
this.renderSourceLoadFuture.cancel(true);
this.renderSourceLoadFuture = null;
}
this.renderSource = null;
}
public void disposeRenderBuffer()
{
cancelBuildBuffer();
if (this.activeRenderBufferRef.get() != null)
{
ColumnRenderBuffer buffer = this.activeRenderBufferRef.getAndSet(null);
buffer.close();
}
}
}
@@ -145,11 +145,12 @@ public class RenderBufferHandler
DhSectionPos sectionPos = node.sectionPos;
LodRenderSection renderSection = node.value;
if (renderSection != null && renderSection.shouldRender())
if (renderSection != null && renderSection.isRenderingEnabled())
{
if (renderSection.renderBufferRef.get() != null && renderSection.renderBufferRef.get().buffersUploaded)
AbstractRenderBuffer buffer = renderSection.activeRenderBufferRef.get();
if (buffer != null)
{
this.loadedNearToFarBuffers.add(new LoadedRenderBuffer(renderSection.renderBufferRef.get(), sectionPos));
this.loadedNearToFarBuffers.add(new LoadedRenderBuffer(buffer, sectionPos));
}
}
}
@@ -168,48 +169,20 @@ public class RenderBufferHandler
public void updateQuadTreeRenderSources()
{
try
Iterator<QuadNode<LodRenderSection>> nodeIterator = this.lodQuadTree.nodeIterator();
while (nodeIterator.hasNext())
{
Iterator<QuadNode<LodRenderSection>> nodeIterator = this.lodQuadTree.nodeIterator();
while (nodeIterator.hasNext())
{
LodRenderSection renderSection = nodeIterator.next().value;
LodRenderSection renderSection = nodeIterator.next().value;
try {
if (renderSection != null)
{
ColumnRenderSource sectionRenderSource = renderSection.getRenderSource();
// if the render source is present, attempt to build it
if (sectionRenderSource != null)
{
ColumnRenderSource[] adjacentRenderSources = new ColumnRenderSource[ELodDirection.ADJ_DIRECTIONS.length];
for (ELodDirection direction : ELodDirection.ADJ_DIRECTIONS)
{
try
{
DhSectionPos adjPos = sectionRenderSource.sectionPos.getAdjacentPos(direction);
LodRenderSection adjRenderSection = this.lodQuadTree.getValue(adjPos);
// adjacent render sources can be null
if (adjRenderSection != null)
{
ColumnRenderSource adjRenderSource = adjRenderSection.getRenderSource();
adjacentRenderSources[direction.ordinal() - 2] = adjRenderSource;
}
}
catch (IndexOutOfBoundsException e)
{
// adjacent positions can be out of bounds, in that case a null render source will be used
}
}
// 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.renderBufferRef, adjacentRenderSources);
}
renderSection.tryBuildAndSwapBuffer(lodQuadTree);
}
}
}
catch (Exception e)
{
LOGGER.error("Error updating QuadTree render sources. Error: "+e.getMessage(), e);
catch (Exception e)
{
LOGGER.error("Error updating QuadTree render source at "+renderSection.pos+".", e);
}
}
}
@@ -219,10 +192,9 @@ public class RenderBufferHandler
while (nodeIterator.hasNext())
{
LodRenderSection renderSection = nodeIterator.next().value;
if (renderSection != null && renderSection.renderBufferRef.get() != null)
if (renderSection != null)
{
renderSection.renderBufferRef.get().close();
renderSection.renderBufferRef.set(null);
renderSection.dispose();
}
}
}