Merge !83 (Improve Chunk Update Queue)

This commit is contained in:
James Seibel
2025-09-05 22:23:25 -05:00
parent a2c6f906fa
commit 5423b49f3d
4 changed files with 453 additions and 308 deletions
@@ -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<IChunkWrapper> getNeighbourChunkListForChunk(IChunkWrapper chunkWrapper, IDhLevel dhLevel)
private static ArrayList<IChunkWrapper> getNeighborChunkListForChunk(IChunkWrapper chunkWrapper, IDhLevel dhLevel)
{
// get the neighboring chunk list
ArrayList<IChunkWrapper> neighbourChunkList = new ArrayList<>(9);
ArrayList<IChunkWrapper> 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<IChunkWrapper> neighbourChunkList, IDhLevel dhLevel)
{ queueChunkUpdate(chunkWrapper, neighbourChunkList, dhLevel,false); }
private static void queueChunkUpdate(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> 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<IChunkWrapper> 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<IChunkWrapper> neighbourChunkList = updateData.neighbourChunkList;
IDhLevel dhLevel = updateData.dhLevel;
// having a list of the nearby chunks is needed for lighting and beacon generation
@Nullable ArrayList<IChunkWrapper> nearbyChunkList = updateData.neighborChunkList;
// a non-null list is needed for the lighting engine
if (nearbyChunkList == null)
{
nearbyChunkList = new ArrayList<IChunkWrapper>();
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<IChunkWrapper> 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<IChunkWrapper> 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<IChunkWrapper> 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<DhChunkPos> closestQueue;
private final PriorityBlockingQueue<DhChunkPos> furthestQueue;
private final ConcurrentHashMap<DhChunkPos, UpdateChunkData> 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+" ]";
}
@@ -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<DhChunkPos> closestQueue;
private final PriorityBlockingQueue<DhChunkPos> furthestQueue;
private final ConcurrentHashMap<DhChunkPos, ChunkUpdateData> 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);
}
}
@@ -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<IChunkWrapper> neighborChunkList;
public IDhLevel dhLevel;
public boolean canGetNeighborChunks;
public ChunkUpdateData(IChunkWrapper chunkWrapper, @Nullable ArrayList<IChunkWrapper> neighborChunkList, IDhLevel dhLevel, boolean canGetNeighborChunks)
{
this.chunkWrapper = chunkWrapper;
this.neighborChunkList = neighborChunkList;
this.dhLevel = dhLevel;
this.canGetNeighborChunks = canGetNeighborChunks;
}
}
@@ -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);
}
}