diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/CachedColumnRenderSource.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/CachedColumnRenderSource.java index 00997875e..4b05bb260 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/CachedColumnRenderSource.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/render/CachedColumnRenderSource.java @@ -1,8 +1,15 @@ package com.seibel.distanthorizons.core.dataObjects.render; import com.google.common.cache.Cache; +import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2; +import com.seibel.distanthorizons.core.dataObjects.transformers.FullDataToRenderDataTransformer; +import com.seibel.distanthorizons.core.file.fullDatafile.FullDataSourceProviderV2; +import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker; +import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; @@ -12,7 +19,11 @@ import java.util.concurrent.locks.ReentrantLock; */ public class CachedColumnRenderSource implements AutoCloseable { - public final ColumnRenderSource columnRenderSource; + /** an externally handled future that will complete once the {@link CachedColumnRenderSource#columnRenderSource} has finished loading */ + public final CompletableFuture loadFuture; + /** will be null initially, should be non-null once the corresponding load future is done */ + @Nullable + public ColumnRenderSource columnRenderSource = null; private final AtomicInteger referenceCount; private final Cache cachedRenderSourceByPos; @@ -25,11 +36,11 @@ public class CachedColumnRenderSource implements AutoCloseable //=============// public CachedColumnRenderSource( - @NotNull ColumnRenderSource columnRenderSource, + @NotNull CompletableFuture loadFuture, @NotNull ReentrantLock getterLock, @NotNull Cache cachedRenderSourceByPos) { - this.columnRenderSource = columnRenderSource; + this.loadFuture = loadFuture; this.getterLock = getterLock; this.referenceCount = new AtomicInteger(1); this.cachedRenderSourceByPos = cachedRenderSourceByPos; @@ -62,6 +73,13 @@ public class CachedColumnRenderSource implements AutoCloseable // lock to prevent other threads for accessing the cache if we invalidate it this.getterLock.lock(); + // should only happen if something goes wrong up-stream + if (this.columnRenderSource == null) + { + return; + } + + // only close once everyone is done with this datasource int refCount = this.referenceCount.decrementAndGet(); if (refCount == 0) diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java b/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java index c47912225..b42ea11b5 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/LodRenderSection.java @@ -211,13 +211,15 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable this.getAndBuildRenderDataRunnable = () -> { this.getAndRefreshRenderingBeacons(); - this.getAndUploadRenderDataToGpu(); - - // the future is passed in separate to prevent any possible race condition null pointers - future.complete(null); - // the task is done, we don't need to track these anymore - this.getAndBuildRenderDataFuture = null; - this.getAndBuildRenderDataRunnable = null; + this.getAndUploadRenderDataToGpuAsync() + .thenRun(() -> + { + // the future is passed in separately (IE not using the local var) to prevent any possible race condition null pointers + future.complete(null); + // the task is done, we don't need to track these anymore + this.getAndBuildRenderDataFuture = null; + this.getAndBuildRenderDataRunnable = null; + }); }; executor.execute(this.getAndBuildRenderDataRunnable); @@ -233,56 +235,80 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable return false; } } - private void getAndUploadRenderDataToGpu() + private CompletableFuture getAndUploadRenderDataToGpuAsync() { - try(CachedColumnRenderSource cachedRenderSource = this.getRenderSourceForPos(this.pos)) - { - if (cachedRenderSource == null) + // get the center pos data + return this.getRenderSourceForPosAsync(this.pos) + .thenCompose((CachedColumnRenderSource cachedRenderSource) -> { - // nothing needs to be rendered - // TODO how doesn't this cause infinite file handler loops? - // to trigger an upload we check if the buffer is null, and we aren't - // setting the render buffer here - return; - } - ColumnRenderSource thisRenderSource = cachedRenderSource.columnRenderSource; - - - boolean enableTransparency = Config.Client.Advanced.Graphics.Quality.transparency.get().transparencyEnabled; - LodQuadBuilder lodQuadBuilder = new LodQuadBuilder(enableTransparency, this.level.getClientLevelWrapper()); - - // load adjacent render sources - try(CachedColumnRenderSource northRenderSource = this.getRenderSourceForPos(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.NORTH)); - CachedColumnRenderSource southRenderSource = this.getRenderSourceForPos(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.SOUTH)); - CachedColumnRenderSource eastRenderSource = this.getRenderSourceForPos(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.EAST)); - CachedColumnRenderSource westRenderSource = this.getRenderSourceForPos(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.WEST))) - { - ColumnRenderSource[] adjacentRenderSections = new ColumnRenderSource[EDhDirection.ADJ_DIRECTIONS.length]; - adjacentRenderSections[EDhDirection.NORTH.ordinal() - 2] = (northRenderSource != null) ? northRenderSource.columnRenderSource : null; - adjacentRenderSections[EDhDirection.SOUTH.ordinal() - 2] = (southRenderSource != null) ? southRenderSource.columnRenderSource : null; - adjacentRenderSections[EDhDirection.EAST.ordinal() - 2] = (eastRenderSource != null) ? eastRenderSource.columnRenderSource : null; - adjacentRenderSections[EDhDirection.WEST.ordinal() - 2] = (westRenderSource != null) ? westRenderSource.columnRenderSource : null; - - boolean[] adjIsSameDetailLevel = new boolean[EDhDirection.ADJ_DIRECTIONS.length]; - adjIsSameDetailLevel[EDhDirection.NORTH.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.NORTH); - adjIsSameDetailLevel[EDhDirection.SOUTH.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.SOUTH); - adjIsSameDetailLevel[EDhDirection.EAST.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.EAST); - adjIsSameDetailLevel[EDhDirection.WEST.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.WEST); - - // the render sources are only needed by this synchronous method, - // then they can be closed - ColumnRenderBufferBuilder.makeLodRenderData(lodQuadBuilder, thisRenderSource, this.level, adjacentRenderSections, adjIsSameDetailLevel); - } - - this.uploadToGpuAsync(lodQuadBuilder); - } - catch (Exception e) - { - LOGGER.error("Unexpected error while loading LodRenderSection ["+DhSectionPos.toString(this.pos)+"], Error: [" + e.getMessage() + "].", e); - } + try + { + if (cachedRenderSource == null || cachedRenderSource.columnRenderSource == null) + { + // nothing needs to be rendered + // TODO how doesn't this cause infinite file handler loops? + // to trigger an upload we check if the buffer is null, and we aren't + // setting the render buffer here + return CompletableFuture.completedFuture(null); + } + ColumnRenderSource thisRenderSource = cachedRenderSource.columnRenderSource; + + + boolean enableTransparency = Config.Client.Advanced.Graphics.Quality.transparency.get().transparencyEnabled; + LodQuadBuilder lodQuadBuilder = new LodQuadBuilder(enableTransparency, this.level.getClientLevelWrapper()); + + + // get the adjacent positions + // needs to be done async to prevent threads waiting on the same positions to be processed + final CompletableFuture[] adjacentLoadFutures = new CompletableFuture[4]; + adjacentLoadFutures[0] = this.getRenderSourceForPosAsync(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.NORTH)); + adjacentLoadFutures[1] = this.getRenderSourceForPosAsync(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.SOUTH)); + adjacentLoadFutures[2] = this.getRenderSourceForPosAsync(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.EAST)); + adjacentLoadFutures[3] = this.getRenderSourceForPosAsync(DhSectionPos.getAdjacentPos(this.pos, EDhDirection.WEST)); + return CompletableFuture.allOf(adjacentLoadFutures).thenRun(() -> + { + try (CachedColumnRenderSource northRenderSource = adjacentLoadFutures[0].get(); + CachedColumnRenderSource southRenderSource = adjacentLoadFutures[1].get(); + CachedColumnRenderSource eastRenderSource = adjacentLoadFutures[2].get(); + CachedColumnRenderSource westRenderSource = adjacentLoadFutures[3].get()) + { + ColumnRenderSource[] adjacentRenderSections = new ColumnRenderSource[EDhDirection.ADJ_DIRECTIONS.length]; + adjacentRenderSections[EDhDirection.NORTH.ordinal() - 2] = (northRenderSource != null) ? northRenderSource.columnRenderSource : null; + adjacentRenderSections[EDhDirection.SOUTH.ordinal() - 2] = (southRenderSource != null) ? southRenderSource.columnRenderSource : null; + adjacentRenderSections[EDhDirection.EAST.ordinal() - 2] = (eastRenderSource != null) ? eastRenderSource.columnRenderSource : null; + adjacentRenderSections[EDhDirection.WEST.ordinal() - 2] = (westRenderSource != null) ? westRenderSource.columnRenderSource : null; + + boolean[] adjIsSameDetailLevel = new boolean[EDhDirection.ADJ_DIRECTIONS.length]; + adjIsSameDetailLevel[EDhDirection.NORTH.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.NORTH); + adjIsSameDetailLevel[EDhDirection.SOUTH.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.SOUTH); + adjIsSameDetailLevel[EDhDirection.EAST.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.EAST); + adjIsSameDetailLevel[EDhDirection.WEST.ordinal() - 2] = this.isAdjacentPosSameDetailLevel(EDhDirection.WEST); + + // the render sources are only needed by this synchronous method, + // then they can be closed + ColumnRenderBufferBuilder.makeLodRenderData(lodQuadBuilder, thisRenderSource, this.level, adjacentRenderSections, adjIsSameDetailLevel); + this.uploadToGpuAsync(lodQuadBuilder); + } + catch (Exception e) + { + LOGGER.error("Unexpected error while loading LodRenderSection [" + DhSectionPos.toString(this.pos) + "] adjacent data, Error: [" + e.getMessage() + "].", e); + } + finally + { + // can only be closed after the data has been processed and uploaded to the GPU + cachedRenderSource.close(); + } + }); + } + catch (Exception e) + { + LOGGER.error("Unexpected error while loading LodRenderSection ["+DhSectionPos.toString(this.pos)+"], Error: [" + e.getMessage() + "].", e); + return CompletableFuture.completedFuture(null); + } + }); } - @Nullable - private CachedColumnRenderSource getRenderSourceForPos(long pos) + /** async is done so each thread can run without waiting on others */ + private CompletableFuture getRenderSourceForPosAsync(long pos) { ReentrantLock lock = this.renderLoadLockContainer.getLockForPos(pos); try @@ -292,26 +318,55 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable lock.lock(); // use the cached data if possible - CachedColumnRenderSource cachedRenderSource = this.cachedRenderSourceByPos.getIfPresent(pos); - if (cachedRenderSource != null) + CachedColumnRenderSource existingCachedRenderSource = this.cachedRenderSourceByPos.getIfPresent(pos); + if (existingCachedRenderSource != null) { - cachedRenderSource.markInUse(); - return cachedRenderSource; + existingCachedRenderSource.markInUse(); + return existingCachedRenderSource.loadFuture; } - // generate new render source - try (FullDataSourceV2 fullDataSource = this.fullDataSourceProvider.get(pos)) + + + PriorityTaskPicker.Executor executor = ThreadPoolUtil.getFileHandlerExecutor(); + if (executor == null || executor.isTerminated()) { - ColumnRenderSource renderSource = FullDataToRenderDataTransformer.transformFullDataToRenderSource(fullDataSource, this.level); - // only add valid data to the cache (to prevent null pointers) - if (renderSource != null) + // should only happen if the threadpool is actively being re-sized + return CompletableFuture.completedFuture(null); + } + + + // queue loading the render data + CompletableFuture loadFuture = new CompletableFuture<>(); + final CachedColumnRenderSource newCachedRenderSource = new CachedColumnRenderSource(loadFuture, lock, this.cachedRenderSourceByPos); + executor.execute(() -> + { + // generate new render source + try (FullDataSourceV2 fullDataSource = this.fullDataSourceProvider.get(pos)) { - cachedRenderSource = new CachedColumnRenderSource(renderSource, lock, this.cachedRenderSourceByPos); - this.cachedRenderSourceByPos.put(pos, cachedRenderSource); + newCachedRenderSource.columnRenderSource = FullDataToRenderDataTransformer.transformFullDataToRenderSource(fullDataSource, this.level); } - } + catch (Exception e) + { + LOGGER.error("Unexpected issue creating render data for pos: ["+DhSectionPos.toString(pos)+"], error: ["+e.getMessage()+"].", e); + } + finally + { + loadFuture.complete(newCachedRenderSource); + } + }); + this.cachedRenderSourceByPos.put(pos, newCachedRenderSource); - return cachedRenderSource; + return loadFuture; + } + catch (RejectedExecutionException ignore) + { + // the thread pool was probably shut down because it's size is being changed, just wait a sec and it should be back + return CompletableFuture.completedFuture(null); + } + catch (Exception e) + { + LOGGER.error("Unexpected issue getting and creating render data for pos: ["+DhSectionPos.toString(pos)+"], error: ["+e.getMessage()+"].", e); + return CompletableFuture.completedFuture(null); } finally {