diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java index d79be8372..7f20849bb 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java @@ -28,6 +28,7 @@ import com.seibel.distanthorizons.core.generation.DhLightingEngine; import com.seibel.distanthorizons.core.level.IDhLevel; import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; import com.seibel.distanthorizons.core.logging.f3.F3Screen; +import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D; import com.seibel.distanthorizons.core.pos.DhChunkPos; import com.seibel.distanthorizons.core.render.renderer.DebugRenderer; @@ -40,12 +41,14 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper; +import com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor.IModChecker; import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper; import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector; import org.apache.logging.log4j.Logger; import org.jetbrains.annotations.Nullable; +import java.awt.*; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.locks.ReentrantLock; @@ -76,6 +79,12 @@ public class SharedApi private static int lastWorldGenTickDelta = 0; private static long lastOverloadedLogMessageMsTime = 0; + /** + * Running async is preferred since it prevents + * lagging the server thread (where most chunk updates occur). + */ + public static boolean runChunkUpdatesAsync = false; + //=============// @@ -286,6 +295,7 @@ public class SharedApi * Config.Common.MultiThreading.numberOfLodBuilderThreads.get() * (playerCount + 1); + boolean dhThreadPoolOverloaded = false; UpdateChunkData updateData = new UpdateChunkData(chunkWrapper, neighbourChunkList, dhLevel, lightUpdateOnly); if(lightUpdateOnly) { @@ -294,6 +304,8 @@ public class SharedApi int remainingCapacity = UPDATE_POS_MANAGER.addItem(chunkWrapper.getChunkPos(), updateData); if (remainingCapacity <= 0) { + dhThreadPoolOverloaded = true; + // limit how often an overloaded message can be sent long msBetweenLastLog = System.currentTimeMillis() - lastOverloadedLogMessageMsTime; if (msBetweenLastLog >= MIN_MS_BETWEEN_OVERLOADED_LOG_MESSAGE) @@ -322,24 +334,45 @@ public class SharedApi } - - // queue updates up to the number of CPU cores allocated for the job - // (this prevents doing extra work queuing tasks that may not be necessary) - // and makes sure the chunks closest to the player are updated first - ThreadPoolExecutor executor = ThreadPoolUtil.getChunkToLodBuilderExecutor(); - if (executor != null && executor.getQueue().size() < executor.getCorePoolSize()) + // prefer to run async to prevent lag, however fallback to running on + // MC's server thread if necessary + if (runChunkUpdatesAsync && !dhThreadPoolOverloaded) { - try + // queue updates up to the number of CPU cores allocated for the job + // (this prevents doing extra work queuing tasks that may not be necessary) + // and makes sure the chunks closest to the player are updated first + ThreadPoolExecutor executor = ThreadPoolUtil.getChunkToLodBuilderExecutor(); + if (executor != null && executor.getQueue().size() < executor.getCorePoolSize()) { - executor.execute(SharedApi::processQueuedChunkUpdate); - } - catch (RejectedExecutionException ignore) - { - // the executor was shut down, it should be back up shortly and able to accept new jobs + try + { + executor.execute(SharedApi::processQueuedChunkUpdate); + } + catch (RejectedExecutionException ignore) + { + // the executor was shut down, it should be back up shortly and able to accept new jobs + } } } + else + { + // when running on the server thread some logic needs to be disabled + // (specifically getting adjacent chunks and queuing a new DH thread task) + // to prevent locking up the server thread + updateData.runSync = true; + + processUpdateChunkData(updateData); + + DebugRenderer.makeParticle( + new DebugRenderer.BoxParticle( + new DebugRenderer.Box(DhSectionPos.encodeContaining(LodUtil.CHUNK_DETAIL_LEVEL, chunkWrapper.getChunkPos()), 128f, 156f, 0.09f, Color.RED) + ,0.2, 32f + ) + ); + + UPDATE_POS_MANAGER.removeItem(chunkWrapper.getChunkPos()); + } } - /** returning a {@link CompletableFuture} isn't necessary, but allows Intellij to properly show the full stack trace when debugging. */ private static void processQueuedChunkUpdate() { //LOGGER.trace(chunkWrapper.getChunkPos() + " " + executor.getActiveCount() + " / " + executor.getQueue().size() + " - " + executor.getCompletedTaskCount()); @@ -350,6 +383,10 @@ public class SharedApi return; } + processUpdateChunkData(updateData); + } + private static void processUpdateChunkData(UpdateChunkData updateData) + { IChunkWrapper chunkWrapper = updateData.chunkWrapper; @Nullable ArrayList neighbourChunkList = updateData.neighbourChunkList; IDhLevel dhLevel = updateData.dhLevel; @@ -385,15 +422,19 @@ public class SharedApi nearbyChunkList.add(chunkWrapper); } - // if this chunk will update its lighting - // then queue adjacent chunks to update theirs as well - // adjacent chunk will have 'lightUpdateOnly' true - // so they won't schedule further chunk updates - if (!updateData.lightUpdateOnly) + // getting neighbor chunks can lock up the server thread + if (!updateData.runSync) { - for (IChunkWrapper adjacentChunk : nearbyChunkList) + // if this chunk will update its lighting + // then queue adjacent chunks to update theirs as well + // adjacent chunk will have 'lightUpdateOnly' true + // so they won't schedule further chunk updates + if (!updateData.lightUpdateOnly) { - queueChunkUpdate(adjacentChunk, getNeighbourChunkListForChunk(adjacentChunk, dhLevel), dhLevel, true); + for (IChunkWrapper adjacentChunk : nearbyChunkList) + { + queueChunkUpdate(adjacentChunk, getNeighbourChunkListForChunk(adjacentChunk, dhLevel), dhLevel, true); + } } } @@ -409,17 +450,21 @@ public class SharedApi } finally { - // queue the next position if there are still positions to process - ThreadPoolExecutor executor = ThreadPoolUtil.getChunkToLodBuilderExecutor(); - if (executor != null && !UPDATE_POS_MANAGER.positionMap.isEmpty()) + // only queue a new DH thread task if this is running on a DH thread + if (!updateData.runSync) { - try + // queue the next position if there are still positions to process + ThreadPoolExecutor executor = ThreadPoolUtil.getChunkToLodBuilderExecutor(); + if (executor != null && !UPDATE_POS_MANAGER.positionMap.isEmpty()) { - executor.execute(SharedApi::processQueuedChunkUpdate); - } - catch (RejectedExecutionException ignore) - { - // the executor was shut down, it should be back up shortly and able to accept new jobs + try + { + executor.execute(SharedApi::processQueuedChunkUpdate); + } + catch (RejectedExecutionException ignore) + { + // the executor was shut down, it should be back up shortly and able to accept new jobs + } } } } @@ -453,6 +498,8 @@ public class SharedApi public IDhLevel dhLevel; /** adjacent chunks will only update their light */ public boolean lightUpdateOnly; + /** If set to true this will run synchronously, probably on the MC server thread */ + public boolean runSync = false; public UpdateChunkData(IChunkWrapper chunkWrapper, @Nullable ArrayList neighbourChunkList, IDhLevel dhLevel, boolean lightUpdateOnly) {