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 106c3e726..079ddb7b4 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 @@ -22,6 +22,8 @@ package com.seibel.distanthorizons.core.api.internal; import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiWorldLoadEvent; import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiWorldUnloadEvent; import com.seibel.distanthorizons.core.Initializer; +import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateData; +import com.seibel.distanthorizons.core.api.internal.chunkUpdating.ChunkUpdateQueueManager; import com.seibel.distanthorizons.core.config.Config; import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector; import com.seibel.distanthorizons.core.generation.DhLightingEngine; @@ -65,20 +67,19 @@ public class SharedApi private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class); private static final IMinecraftSharedWrapper MC_SHARED = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class); - private static final UpdateChunkPosManager UPDATE_POS_MANAGER = new UpdateChunkPosManager(); + public static final ChunkUpdateQueueManager CHUNK_UPDATE_QUEUE_MANAGER = new ChunkUpdateQueueManager(); /** * how many chunks can be queued for updating per thread + player (in multiplayer), * used to prevent updates from infinitely pilling up if the user flies around extremely fast */ - private static final int MAX_UPDATING_CHUNK_COUNT_PER_THREAD_AND_PLAYER = 1_000; + public static final int MAX_UPDATING_CHUNK_COUNT_PER_THREAD_AND_PLAYER = 1_000; /** how many milliseconds must pass before an overloaded message can be sent in chat or the log */ - private static final int MIN_MS_BETWEEN_OVERLOADED_LOG_MESSAGE = 30_000; + public static final int MIN_MS_BETWEEN_OVERLOADED_LOG_MESSAGE = 30_000; private static AbstractDhWorld currentWorld; private static int lastWorldGenTickDelta = 0; - private static long lastOverloadedLogMessageMsTime = 0; @@ -127,7 +128,7 @@ public class SharedApi // shouldn't be necessary, but if we missed closing one of the connections this should make sure they're all closed AbstractDhRepo.closeAllConnections(); // needs to be closed on world shutdown to clear out un-processed chunks - UPDATE_POS_MANAGER.clear(); + CHUNK_UPDATE_QUEUE_MANAGER.clear(); // recommend that the garbage collector cleans up any objects from the old world and thread pools System.gc(); @@ -168,10 +169,10 @@ public class SharedApi * This is important since asking MC for a chunk is slow and may block the render thread. */ public static boolean isChunkAtBlockPosAlreadyUpdating(int blockPosX, int blockPosZ) - { return UPDATE_POS_MANAGER.contains(new DhChunkPos(new DhBlockPos2D(blockPosX, blockPosZ))); } + { return CHUNK_UPDATE_QUEUE_MANAGER.contains(new DhChunkPos(new DhBlockPos2D(blockPosX, blockPosZ))); } public static boolean isChunkAtChunkPosAlreadyUpdating(int chunkPosX, int chunkPosZ) - { return UPDATE_POS_MANAGER.contains(new DhChunkPos(chunkPosX, chunkPosZ)); } + { return CHUNK_UPDATE_QUEUE_MANAGER.contains(new DhChunkPos(chunkPosX, chunkPosZ)); } /** * This is often fired when unloading a level. @@ -179,9 +180,9 @@ public class SharedApi * rapidly changing dimensions. * (IE prevent DH from infinitely allocating memory */ - public void clearQueuedChunkUpdates() { UPDATE_POS_MANAGER.clear(); } + public void clearQueuedChunkUpdates() { CHUNK_UPDATE_QUEUE_MANAGER.clear(); } - public int getQueuedChunkUpdateCount() { return UPDATE_POS_MANAGER.closestQueue.size(); } + public int getQueuedChunkUpdateCount() { return CHUNK_UPDATE_QUEUE_MANAGER.getQueuedCount(); } @@ -189,7 +190,7 @@ public class SharedApi public void chunkBlockChangedEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true); } public void chunkLoadEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, false); } - public void applyChunkUpdate(IChunkWrapper chunkWrapper, ILevelWrapper level, boolean updateNeighborChunks) + public void applyChunkUpdate(IChunkWrapper chunkWrapper, ILevelWrapper level, boolean canGetNeighborChunks) { //========================// // world and level checks // @@ -245,35 +246,18 @@ public class SharedApi } // shoudln't normally happen, but just in case - if (UPDATE_POS_MANAGER.contains(chunkWrapper.getChunkPos())) + if (CHUNK_UPDATE_QUEUE_MANAGER.contains(chunkWrapper.getChunkPos())) { // TODO this will prevent some LODs from updating across dimensions if multiple levels are loaded return; } - - - //===============================// - // update the necessary chunk(s) // - //===============================// - - if (!updateNeighborChunks) - { - // only update the center chunk - - queueChunkUpdate(chunkWrapper, null, dhLevel); - } - else - { - // update the center with any existing neighbour chunks. - // this is done so lighting changes are propagated correctly - queueChunkUpdate(chunkWrapper, getNeighbourChunkListForChunk(chunkWrapper,dhLevel), dhLevel); - } + queueChunkUpdate(chunkWrapper, dhLevel, canGetNeighborChunks); } - private static ArrayList getNeighbourChunkListForChunk(IChunkWrapper chunkWrapper, IDhLevel dhLevel) + private static ArrayList getNeighborChunkListForChunk(IChunkWrapper chunkWrapper, IDhLevel dhLevel) { // get the neighboring chunk list - ArrayList neighbourChunkList = new ArrayList<>(9); + ArrayList neighborChunkList = new ArrayList<>(9); for (int xOffset = -1; xOffset <= 1; xOffset++) { for (int zOffset = -1; zOffset <= 1; zOffset++) @@ -281,80 +265,34 @@ public class SharedApi if (xOffset == 0 && zOffset == 0) { // center chunk - neighbourChunkList.add(chunkWrapper); + neighborChunkList.add(chunkWrapper); } else { - // neighboring chunk - DhChunkPos neighbourPos = new DhChunkPos(chunkWrapper.getChunkPos().getX() + xOffset, chunkWrapper.getChunkPos().getZ() + zOffset); - IChunkWrapper neighbourChunk = dhLevel.getLevelWrapper().tryGetChunk(neighbourPos); - if (neighbourChunk != null) + // neighboring chunk + DhChunkPos neighborPos = new DhChunkPos(chunkWrapper.getChunkPos().getX() + xOffset, chunkWrapper.getChunkPos().getZ() + zOffset); + IChunkWrapper neighborChunk = dhLevel.getLevelWrapper().tryGetChunk(neighborPos); + if (neighborChunk != null) { - neighbourChunkList.add(neighbourChunk); + neighborChunkList.add(neighborChunk); } } } } - return neighbourChunkList; + return neighborChunkList; } - private static void queueChunkUpdate(IChunkWrapper chunkWrapper, @Nullable ArrayList neighbourChunkList, IDhLevel dhLevel) - { queueChunkUpdate(chunkWrapper, neighbourChunkList, dhLevel,false); } - private static void queueChunkUpdate(IChunkWrapper chunkWrapper, @Nullable ArrayList neighbourChunkList, IDhLevel dhLevel, boolean lightUpdateOnly) + private static void queueChunkUpdate(IChunkWrapper chunkWrapper, IDhLevel dhLevel,boolean canGetNeighborChunks) { - int maxUpdateSizeMultiplier; - if (MC_CLIENT != null && MC_CLIENT.playerExists()) + // return if the chunk is already queued + if (CHUNK_UPDATE_QUEUE_MANAGER.contains(chunkWrapper.getChunkPos())) { - // Local worlds & multiplayer - UPDATE_POS_MANAGER.setCenter(MC_CLIENT.getPlayerChunkPos()); - maxUpdateSizeMultiplier = MC_CLIENT.clientConnectedToDedicatedServer() ? 1 : MC_SHARED.getPlayerCount(); - } - else - { - // Dedicated servers - // Also includes spawn chunks since they're likely to be intentionally utilized with updates - maxUpdateSizeMultiplier = 1 + MC_SHARED.getPlayerCount(); - } - - UPDATE_POS_MANAGER.maxSize = MAX_UPDATING_CHUNK_COUNT_PER_THREAD_AND_PLAYER - * Config.Common.MultiThreading.numberOfThreads.get() - * maxUpdateSizeMultiplier; - - UpdateChunkData updateData = new UpdateChunkData(chunkWrapper, neighbourChunkList, dhLevel, lightUpdateOnly); - if(lightUpdateOnly) - { - UPDATE_POS_MANAGER.removeItem(chunkWrapper.getChunkPos()); - } - int remainingCapacity = UPDATE_POS_MANAGER.addItem(chunkWrapper.getChunkPos(), updateData); - if (remainingCapacity <= 0) - { - // limit how often an overloaded message can be sent - long msBetweenLastLog = System.currentTimeMillis() - lastOverloadedLogMessageMsTime; - if (msBetweenLastLog >= MIN_MS_BETWEEN_OVERLOADED_LOG_MESSAGE) - { - lastOverloadedLogMessageMsTime = System.currentTimeMillis(); - - String message = "\u00A76" + "Distant Horizons overloaded, too many chunks queued for LOD processing. " + "\u00A7r" + - "\nThis may result in holes in your LODs. " + - "\nFix: move through the world slower, decrease your vanilla render distance, slow down your world pre-generator (IE Chunky), or increase the Distant Horizons' CPU thread counts. " + - "\nMax queue count ["+UPDATE_POS_MANAGER.maxSize+"] (["+ MAX_UPDATING_CHUNK_COUNT_PER_THREAD_AND_PLAYER +"] per thread+players)."; - - boolean showWarningInChat = Config.Common.Logging.Warning.showUpdateQueueOverloadedChatWarning.get(); - if (showWarningInChat) - { - ClientApi.INSTANCE.showChatMessageNextFrame(message); - } - - // Don't log warnings in singleplayer or in hosted LAN since it usually isn't a problem (and if it is it's easy to notice). - // Servers should always log since being overloaded is harder to notice. - EWorldEnvironment environment = SharedApi.getEnvironment(); - if (showWarningInChat || environment == EWorldEnvironment.SERVER_ONLY) - { - LOGGER.warn(message); - } - } + return; } + // add chunk update data to preUpdate queue + ChunkUpdateData updateData = new ChunkUpdateData(chunkWrapper,null,dhLevel,canGetNeighborChunks); + CHUNK_UPDATE_QUEUE_MANAGER.addItemToPreUpdateQueue(chunkWrapper.getChunkPos(), updateData); // queue updates up to the number of CPU cores allocated for the job @@ -365,7 +303,7 @@ public class SharedApi { try { - executor.execute(SharedApi::processQueuedChunkUpdate); + executor.execute(SharedApi::processQueue); } catch (RejectedExecutionException ignore) { @@ -373,94 +311,150 @@ public class SharedApi } } } + + private static void processQueue() + { + // update the center & max size of the queue manager + int maxUpdateSizeMultiplier; + if (MC_CLIENT != null && MC_CLIENT.playerExists()) + { + // Local worlds & multiplayer + CHUNK_UPDATE_QUEUE_MANAGER.setCenter(MC_CLIENT.getPlayerChunkPos()); + maxUpdateSizeMultiplier = MC_CLIENT.clientConnectedToDedicatedServer() ? 1 : MC_SHARED.getPlayerCount(); + } + else + { + // Dedicated servers + // Also includes spawn chunks since they're likely to be intentionally utilized with updates + maxUpdateSizeMultiplier = 1 + MC_SHARED.getPlayerCount(); + } + CHUNK_UPDATE_QUEUE_MANAGER.maxSize = MAX_UPDATING_CHUNK_COUNT_PER_THREAD_AND_PLAYER + * Config.Common.MultiThreading.numberOfThreads.get() + * maxUpdateSizeMultiplier; + + + + //===============================// + // update the necessary chunk(s) // + //===============================// + + // process preUpdate queue + processQueuedChunkPreUpdate(); + + // process update queue + processQueuedChunkUpdate(); + + // queue the next position if there are still positions to process + AbstractExecutorService executor = ThreadPoolUtil.getChunkToLodBuilderExecutor(); + if (executor != null && !CHUNK_UPDATE_QUEUE_MANAGER.isEmpty()) + { + try + { + executor.execute(SharedApi::processQueue); + } + catch (RejectedExecutionException ignore) + { + // the executor was shut down, it should be back up shortly and able to accept new jobs + } + } + + } + + private static void processQueuedChunkPreUpdate() + { + ChunkUpdateData preUpdateData = CHUNK_UPDATE_QUEUE_MANAGER.preUpdateQueue.popClosest(); + if (preUpdateData == null) + { + return; + } + + IDhLevel dhLevel = preUpdateData.dhLevel; + IChunkWrapper chunkWrapper = preUpdateData.chunkWrapper; + boolean canGetNeighborChunks = preUpdateData.canGetNeighborChunks; + + try + { + // get neighbor chunks if possible + ArrayList neighborChunkList = (canGetNeighborChunks) ? getNeighborChunkListForChunk(chunkWrapper, dhLevel) : null; + preUpdateData.neighborChunkList = neighborChunkList; + + // check if this chunk has been converted into an LOD already + boolean checkChunkHash = !Config.Common.LodBuilding.disableUnchangedChunkCheck.get(); + if (checkChunkHash) + { + int oldChunkHash = dhLevel.getChunkHash(chunkWrapper.getChunkPos()); // shouldn't happen on the render thread since it may take a few moments to run + int newChunkHash = chunkWrapper.getBlockBiomeHashCode(); + + boolean hasNewChunkHash = (oldChunkHash != newChunkHash); + if (!hasNewChunkHash) + { + // do not update the chunk if the hash is the same + return; + } + + if (canGetNeighborChunks) + { + // if this chunk will update and can get neighbors + // then queue neighboring chunks to update as well + // neighboring chunks will get added directly to the update queue + // so they won't queue further chunk updates + for (IChunkWrapper adjacentChunk : neighborChunkList) + { + // pulling a new chunkWrapper is necessary to prevent concurrent modification on the existing chunkWrappers + IChunkWrapper newCenterChunk = dhLevel.getLevelWrapper().tryGetChunk(adjacentChunk.getChunkPos()); + if (newCenterChunk != null) + { + ChunkUpdateData newUpdateData = new ChunkUpdateData(newCenterChunk, getNeighborChunkListForChunk(newCenterChunk, dhLevel), dhLevel, true); + CHUNK_UPDATE_QUEUE_MANAGER.addItemToUpdateQueue(newCenterChunk.getChunkPos(), newUpdateData); + } + } + } + } + + CHUNK_UPDATE_QUEUE_MANAGER.addItemToUpdateQueue(chunkWrapper.getChunkPos(), preUpdateData); + } + catch (Exception e) + { + LOGGER.error("Unexpected error when pre-updating chunk at pos: [" + chunkWrapper.getChunkPos() + "]", e); + } + } + private static void processQueuedChunkUpdate() { //LOGGER.trace(chunkWrapper.getChunkPos() + " " + executor.getActiveCount() + " / " + executor.getQueue().size() + " - " + executor.getCompletedTaskCount()); - UpdateChunkData updateData = UPDATE_POS_MANAGER.popClosest(); + ChunkUpdateData updateData = CHUNK_UPDATE_QUEUE_MANAGER.updateQueue.popClosest(); if (updateData == null) { return; } IChunkWrapper chunkWrapper = updateData.chunkWrapper; - @Nullable ArrayList neighbourChunkList = updateData.neighbourChunkList; IDhLevel dhLevel = updateData.dhLevel; + // having a list of the nearby chunks is needed for lighting and beacon generation + @Nullable ArrayList nearbyChunkList = updateData.neighborChunkList; + // a non-null list is needed for the lighting engine + if (nearbyChunkList == null) + { + nearbyChunkList = new ArrayList(); + nearbyChunkList.add(chunkWrapper); + } try { - boolean checkChunkHash = !Config.Common.LodBuilding.disableUnchangedChunkCheck.get(); - - // check if this chunk has been converted into an LOD already - int oldChunkHash = dhLevel.getChunkHash(chunkWrapper.getChunkPos()); // shouldn't happen on the render thread since it may take a few moments to run - int newChunkHash = chunkWrapper.getBlockBiomeHashCode(); - if (checkChunkHash) - { - if (oldChunkHash == newChunkHash && !updateData.lightUpdateOnly) - { - // if the chunk hashes are the same then we don't need to bother with lighting the chunk - // or creating/updating the LODs - return; - } - } - - - // having a list of the nearby chunks is needed for lighting and beacon generation - ArrayList nearbyChunkList; - if (neighbourChunkList != null) - { - nearbyChunkList = neighbourChunkList; - } - else - { - nearbyChunkList = new ArrayList<>(1); - 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) - { - for (IChunkWrapper adjacentChunk : nearbyChunkList) - { - // pulling a new chunkWrapper is necessary to prevent concurrent modification on the existing chunkWrappers - IChunkWrapper newCenterChunk = dhLevel.getLevelWrapper().tryGetChunk(adjacentChunk.getChunkPos()); - if (newCenterChunk != null) - { - queueChunkUpdate(newCenterChunk, getNeighbourChunkListForChunk(newCenterChunk, dhLevel), dhLevel, true); - } - } - } - // sky lighting is populated later at the data source level DhLightingEngine.INSTANCE.bakeChunkBlockLighting(chunkWrapper, nearbyChunkList, dhLevel.hasSkyLight() ? LodUtil.MAX_MC_LIGHT : LodUtil.MIN_MC_LIGHT); dhLevel.updateBeaconBeamsForChunk(chunkWrapper, nearbyChunkList); + + int newChunkHash = chunkWrapper.getBlockBiomeHashCode(); dhLevel.updateChunkAsync(chunkWrapper, newChunkHash); } catch (Exception e) { LOGGER.error("Unexpected error when updating chunk at pos: [" + chunkWrapper.getChunkPos() + "]", e); } - finally - { - // queue the next position if there are still positions to process - AbstractExecutorService executor = ThreadPoolUtil.getChunkToLodBuilderExecutor(); - if (executor != null && !UPDATE_POS_MANAGER.updateDataByChunkPos.isEmpty()) - { - 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 - } - } - } } @@ -471,161 +465,13 @@ public class SharedApi public String getDebugMenuString() { - String updatingCountStr = F3Screen.NUMBER_FORMAT.format(UPDATE_POS_MANAGER.closestQueue.size()); - String maxUpdateCountStr = F3Screen.NUMBER_FORMAT.format(UPDATE_POS_MANAGER.maxSize); - return "Queued chunk updates: "+updatingCountStr+" / "+maxUpdateCountStr; - } - - - - //================// - // helper classes // - //================// - - /** contains the objects needed to update a chunk */ - private static class UpdateChunkData - { - public IChunkWrapper chunkWrapper; - @Nullable - public ArrayList neighbourChunkList; - public IDhLevel dhLevel; - /** adjacent chunks will only update their light */ - public boolean lightUpdateOnly; + String preUpdatingCountStr = F3Screen.NUMBER_FORMAT.format(CHUNK_UPDATE_QUEUE_MANAGER.preUpdateQueue.getQueuedCount()); + String updatingCountStr = F3Screen.NUMBER_FORMAT.format(CHUNK_UPDATE_QUEUE_MANAGER.updateQueue.getQueuedCount()); + String queuedCountStr = F3Screen.NUMBER_FORMAT.format(CHUNK_UPDATE_QUEUE_MANAGER.getQueuedCount()); - public UpdateChunkData(IChunkWrapper chunkWrapper, @Nullable ArrayList neighbourChunkList, IDhLevel dhLevel, boolean lightUpdateOnly) - { - this.chunkWrapper = chunkWrapper; - this.neighbourChunkList = neighbourChunkList; - this.dhLevel = dhLevel; - this.lightUpdateOnly = lightUpdateOnly; - } - } - - /** keeps track of which chunks need to be updated */ - private static class UpdateChunkPosManager - { - private final PriorityBlockingQueue closestQueue; - private final PriorityBlockingQueue furthestQueue; - private final ConcurrentHashMap updateDataByChunkPos; - - private DhChunkPos center; - private int maxSize = 500; - - - - //=============// - // constructor // - //=============// - - public UpdateChunkPosManager() - { - this.closestQueue = new PriorityBlockingQueue<>(500, Comparator.comparingDouble(pos -> pos.squaredDistance(this.center))); - this.furthestQueue = new PriorityBlockingQueue<>(500, Comparator.comparingDouble(pos -> ((DhChunkPos)pos).squaredDistance(this.center)).reversed()); - this.updateDataByChunkPos = new ConcurrentHashMap<>(); - // defaulting to 0,0 is fine since it'll be updated once we start adding items - this.center = new DhChunkPos(0, 0); - } - - - - //==================// - // list/set methods // - //==================// - - public boolean contains(DhChunkPos pos) { return this.updateDataByChunkPos.containsKey(pos); } - - public void clear() - { - this.updateDataByChunkPos.clear(); - this.closestQueue.clear(); - this.furthestQueue.clear(); - } - - public void removeItem(DhChunkPos pos) - { - this.updateDataByChunkPos.remove(pos); - this.closestQueue.remove(pos); - this.furthestQueue.remove(pos); - } - - /** - * Adds an item to the queue of chunks that need to be updated. - * If there are no more slots, replaces the item furthest from the center. - * - * @return The number of remaining slots available in the queue. - */ - public int addItem(DhChunkPos pos, UpdateChunkData updateData) - { - int remainingSlots = this.maxSize - this.updateDataByChunkPos.size(); - if (this.updateDataByChunkPos.containsKey(pos)) - { - // Chunk is already present in queue, no need to insert - return remainingSlots; - } - - // If no slots are left, get one by removing the item furthest from the center - if (remainingSlots <= 0) - { - DhChunkPos furthest = this.furthestQueue.poll(); - if (furthest != null) - { - this.closestQueue.remove(furthest); - this.updateDataByChunkPos.remove(furthest); - } - } - - this.updateDataByChunkPos.put(pos, updateData); - this.closestQueue.add(pos); - this.furthestQueue.add(pos); - - return remainingSlots; - } - - - - //==================// - // position methods // - //==================// - - public void setCenter(DhChunkPos newCenter) - { - // if the rebuild time takes too long - // (in James' testing a queue of 500 items only took around 0.1 milliseconds) - // this equation could be changed to only update after moving 2 or 4 chunks from the center - if (newCenter.equals(this.center)) - { - return; - } - - this.center = newCenter; - - // rebuild the priority queues to match the new center - this.closestQueue.clear(); - this.furthestQueue.clear(); - for (DhChunkPos pos : this.updateDataByChunkPos.keySet()) - { - this.closestQueue.add(pos); - this.furthestQueue.add(pos); - } - } - - public UpdateChunkData popClosest() - { - if (this.closestQueue.isEmpty()) - { - return null; - } - - DhChunkPos closest = this.closestQueue.poll(); - if (closest == null) - { - return null; - } - - this.furthestQueue.remove(closest); - return this.updateDataByChunkPos.remove(closest); - } + String maxUpdateCountStr = F3Screen.NUMBER_FORMAT.format(CHUNK_UPDATE_QUEUE_MANAGER.maxSize); + return "Queued chunk updates: "+"( "+preUpdatingCountStr+" + "+updatingCountStr+" ) [ "+queuedCountStr+" / "+maxUpdateCountStr+" ]"; } diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/chunkUpdating/ChunkPosQueue.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/chunkUpdating/ChunkPosQueue.java new file mode 100644 index 000000000..bc99313ac --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/chunkUpdating/ChunkPosQueue.java @@ -0,0 +1,128 @@ +package com.seibel.distanthorizons.core.api.internal.chunkUpdating; + +import com.seibel.distanthorizons.core.api.internal.SharedApi; +import com.seibel.distanthorizons.core.pos.DhChunkPos; + +import java.util.Comparator; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.PriorityBlockingQueue; + +public class ChunkPosQueue +{ + private final PriorityBlockingQueue closestQueue; + private final PriorityBlockingQueue furthestQueue; + private final ConcurrentHashMap updateDataByChunkPos; + + private DhChunkPos center; + + + + //=============// + // constructor // + //=============// + + public ChunkPosQueue() + { + this.closestQueue = new PriorityBlockingQueue<>(500, Comparator.comparingDouble(pos -> pos.squaredDistance(this.center))); + this.furthestQueue = new PriorityBlockingQueue<>(500, Comparator.comparingDouble(pos -> ((DhChunkPos)pos).squaredDistance(this.center)).reversed()); + this.updateDataByChunkPos = new ConcurrentHashMap<>(); + // defaulting to 0,0 is fine since it'll be updated once we start adding items + this.center = new DhChunkPos(0, 0); + } + + + + //==============// + // list methods // + //==============// + + public boolean contains(DhChunkPos pos) { return this.updateDataByChunkPos.containsKey(pos); } + + public void clear() + { + this.updateDataByChunkPos.clear(); + this.closestQueue.clear(); + this.furthestQueue.clear(); + } + + public void addItem(DhChunkPos pos, ChunkUpdateData updateData) + { + if (this.updateDataByChunkPos.containsKey(pos)) + { + // Chunk is already present in queue, no need to insert + return; + } + this.updateDataByChunkPos.put(pos, updateData); + this.closestQueue.add(pos); + this.furthestQueue.add(pos); + } + + public int getQueuedCount() { return this.updateDataByChunkPos.size(); } + + public boolean isEmpty() { return this.updateDataByChunkPos.isEmpty(); } + + + + //==================// + // position methods // + //==================// + + public void setCenter(DhChunkPos newCenter) + { + // if the rebuild time takes too long + // (in James' testing a queue of 500 items only took around 0.1 milliseconds) + // this equation could be changed to only update after moving 2 or 4 chunks from the center + if (newCenter.equals(this.center)) + { + return; + } + + this.center = newCenter; + + // rebuild the priority queues to match the new center + this.closestQueue.clear(); + this.furthestQueue.clear(); + for (DhChunkPos pos : this.updateDataByChunkPos.keySet()) + { + this.closestQueue.add(pos); + this.furthestQueue.add(pos); + } + } + + public ChunkUpdateData popClosest() + { + if (this.closestQueue.isEmpty()) + { + return null; + } + + DhChunkPos closest = this.closestQueue.poll(); + if (closest == null) + { + return null; + } + + this.furthestQueue.remove(closest); + return this.updateDataByChunkPos.remove(closest); + } + public ChunkUpdateData popFurthest() + { + if (this.furthestQueue.isEmpty()) + { + return null; + } + + DhChunkPos furthest = this.furthestQueue.poll(); + if (furthest == null) + { + return null; + } + + this.closestQueue.remove(furthest); + return this.updateDataByChunkPos.remove(furthest); + } + + + +} + diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/chunkUpdating/ChunkUpdateData.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/chunkUpdating/ChunkUpdateData.java new file mode 100644 index 000000000..00e9e0cc1 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/chunkUpdating/ChunkUpdateData.java @@ -0,0 +1,26 @@ +package com.seibel.distanthorizons.core.api.internal.chunkUpdating; + +import com.seibel.distanthorizons.core.level.IDhLevel; +import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; + +public class ChunkUpdateData +{ + public IChunkWrapper chunkWrapper; + @Nullable + public ArrayList neighborChunkList; + public IDhLevel dhLevel; + public boolean canGetNeighborChunks; + + + + public ChunkUpdateData(IChunkWrapper chunkWrapper, @Nullable ArrayList neighborChunkList, IDhLevel dhLevel, boolean canGetNeighborChunks) + { + this.chunkWrapper = chunkWrapper; + this.neighborChunkList = neighborChunkList; + this.dhLevel = dhLevel; + this.canGetNeighborChunks = canGetNeighborChunks; + } +} diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/chunkUpdating/ChunkUpdateQueueManager.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/chunkUpdating/ChunkUpdateQueueManager.java new file mode 100644 index 000000000..72ee82426 --- /dev/null +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/chunkUpdating/ChunkUpdateQueueManager.java @@ -0,0 +1,145 @@ +package com.seibel.distanthorizons.core.api.internal.chunkUpdating; + +import com.seibel.distanthorizons.core.api.internal.ClientApi; +import com.seibel.distanthorizons.core.api.internal.SharedApi; +import com.seibel.distanthorizons.core.config.Config; +import com.seibel.distanthorizons.core.logging.DhLoggerBuilder; +import com.seibel.distanthorizons.core.pos.DhChunkPos; +import com.seibel.distanthorizons.core.world.EWorldEnvironment; +import org.apache.logging.log4j.Logger; + +public class ChunkUpdateQueueManager +{ + private static final Logger LOGGER = DhLoggerBuilder.getLogger(); + + + public final ChunkPosQueue updateQueue; + public final ChunkPosQueue preUpdateQueue; + + public int maxSize = 500; + + private static long lastOverloadedLogMessageMsTime = 0; + + + + //=============// + // constructor // + //=============// + + public ChunkUpdateQueueManager() + { + this.updateQueue = new ChunkPosQueue(); + this.preUpdateQueue = new ChunkPosQueue(); + } + + + + //==================// + // list/set methods // + //==================// + + public boolean contains(DhChunkPos pos) { return this.updateQueue.contains(pos) || this.preUpdateQueue.contains(pos); } + + public void clear() + { + this.updateQueue.clear(); + this.preUpdateQueue.clear(); + } + public int getQueuedCount() { return this.updateQueue.getQueuedCount() + this.preUpdateQueue.getQueuedCount(); } + public boolean isEmpty() + { + return this.updateQueue.isEmpty() + && this.preUpdateQueue.isEmpty(); + } + + /** + * Adds an item to the pre-update queue of chunks that might need to be updated. + * If there are no more slots, replaces the item furthest from the center in the update queue. + */ + public void addItemToPreUpdateQueue(DhChunkPos pos, ChunkUpdateData updateData) + { + int remainingSlots = this.maxSize - this.getQueuedCount(); + + // If no slots are left, get one by removing the item furthest from the center + if (remainingSlots <= 0) + { + if (!this.updateQueue.isEmpty()) + { + this.updateQueue.popFurthest(); + } + else + { + this.preUpdateQueue.popFurthest(); + } + } + this.preUpdateQueue.addItem(pos, updateData); + + remainingSlots = this.maxSize - this.getQueuedCount(); + if (remainingSlots <= 0) + { + this.sendOverloadMessage(); + } + } + + public void addItemToUpdateQueue(DhChunkPos pos, ChunkUpdateData updateData) + { + int remainingSlots = this.maxSize - this.getQueuedCount(); + + // If no slots are left, get one by removing the item furthest from the center + if (remainingSlots <= 0) + { + this.updateQueue.popFurthest(); + } + + this.updateQueue.addItem(pos,updateData); + + remainingSlots = this.maxSize - this.getQueuedCount(); + if (remainingSlots <= 0) + { + this.sendOverloadMessage(); + } + } + + private void sendOverloadMessage() + { + // limit how often an overloaded message can be sent + long msBetweenLastLog = System.currentTimeMillis() - lastOverloadedLogMessageMsTime; + if (msBetweenLastLog >= SharedApi.MIN_MS_BETWEEN_OVERLOADED_LOG_MESSAGE) + { + lastOverloadedLogMessageMsTime = System.currentTimeMillis(); + + String message = "\u00A76" + "Distant Horizons overloaded, too many chunks queued for LOD processing. " + "\u00A7r" + + "\nThis may result in holes in your LODs. " + + "\nFix: move through the world slower, decrease your vanilla render distance, slow down your world pre-generator (IE Chunky), or increase the Distant Horizons' CPU thread counts. " + + "\nMax queue count [" + SharedApi.CHUNK_UPDATE_QUEUE_MANAGER.maxSize + "] ([" + SharedApi.MAX_UPDATING_CHUNK_COUNT_PER_THREAD_AND_PLAYER + "] per thread+players)."; + + boolean showWarningInChat = Config.Common.Logging.Warning.showUpdateQueueOverloadedChatWarning.get(); + if (showWarningInChat) + { + ClientApi.INSTANCE.showChatMessageNextFrame(message); + } + + // Don't log warnings in singleplayer or in hosted LAN since it usually isn't a problem (and if it is it's easy to notice). + // Servers should always log since being overloaded is harder to notice. + EWorldEnvironment environment = SharedApi.getEnvironment(); + if (showWarningInChat || environment == EWorldEnvironment.SERVER_ONLY) + { + LOGGER.warn(message); + } + } + } + + + + //==================// + // position methods // + //==================// + + public void setCenter(DhChunkPos newCenter) + { + this.updateQueue.setCenter(newCenter); + this.preUpdateQueue.setCenter(newCenter); + } + + +}