move beacon render enabling to LodQuadTree

Fixes beacons not always showing/hiding correctly
This commit is contained in:
James Seibel
2026-03-14 14:32:19 -05:00
parent aa53835772
commit 8a39610b8c
4 changed files with 291 additions and 296 deletions
@@ -38,6 +38,8 @@ import com.seibel.distanthorizons.core.render.RenderThreadTaskHandler;
import com.seibel.distanthorizons.core.render.renderer.AbstractDebugWireframeRenderer;
import com.seibel.distanthorizons.core.render.renderer.BeaconRenderHandler;
import com.seibel.distanthorizons.core.render.renderer.IDebugRenderable;
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.ThreadUtil;
import com.seibel.distanthorizons.core.util.WorldGenUtil;
@@ -55,10 +57,7 @@ import org.jetbrains.annotations.Nullable;
import javax.annotation.WillNotClose;
import java.awt.*;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
@@ -97,8 +96,19 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
private final AtomicBoolean queueThreadRunningRef = new AtomicBoolean(false);
/**
* contains the list of beacons currently being rendered in this section
* if this list is modified the {@link LodQuadTree#beaconRenderHandler} should be updated to match.
*/
private final ArrayList<BeaconRenderHandler.BeaconBeamWithWidth> beaconList = new ArrayList<>();
@Nullable
public final BeaconRenderHandler beaconRenderHandler;
private final BeaconRenderHandler beaconRenderHandler;
@Nullable
private final BeaconBeamRepo beaconBeamRepo;
/** used to prevent updating the beacons concurrently */
@NotNull
private CompletableFuture<Void> beaconUpdateFuture = CompletableFuture.completedFuture(null);
/** the smallest numerical detail level number that can be rendered */
private byte maxLeafRenderDetailLevel;
@@ -149,6 +159,8 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
IDhGenericRenderer genericObjectRenderer = this.level.getGenericRenderer();
this.beaconRenderHandler = (genericObjectRenderer != null) ? new BeaconRenderHandler(genericObjectRenderer) : null;
this.beaconBeamRepo = this.level.getBeaconBeamRepo();
Config.Common.WorldGenerator.enableDistantGeneration.addListener(this);
Config.Server.enableServerGeneration.addListener(this);
@@ -367,7 +379,6 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
if (node == null || node.value == null) { continue; }
node.value.setRenderingEnabled(false);
node.value.tryDisableBeacons();
}
for (QuadNode<LodRenderSection> node : this.tickNodeHolder.getEnableDeleteChildrenNodes())
@@ -389,7 +400,6 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
if (childRenderSection != null)
{
childRenderSection.setRenderingEnabled(false);
childRenderSection.tryDisableBeacons();
childRenderSection.close();
}
});
@@ -405,21 +415,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
//=================//
//region
// must be handled after beacon disabling
// otherwise the beacons will be missing
for (QuadNode<LodRenderSection> node : this.tickNodeHolder.getEnabledNodes())
{
if (node == null || node.value == null) { continue; }
node.value.tryEnableBeacons();
}
for (QuadNode<LodRenderSection> node : this.tickNodeHolder.getEnableDeleteChildrenNodes())
{
if (node == null || node.value == null) { continue; }
node.value.tryEnableBeacons();
}
this.tryRefreshRenderingBeaconsAsync(playerPos);
//endregion
@@ -872,6 +868,133 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
//=================//
// beacon handling //
//=================//
//region beacon handling
/** gets the active beacon list and stops/starts beacon rendering as necessary */
private void tryRefreshRenderingBeaconsAsync(DhBlockPos2D playerPos)
{
// do nothing if beacon rendering or repos are unavailable
if (this.beaconBeamRepo == null
|| this.beaconRenderHandler == null)
{
return;
}
AbstractExecutorService executor = ThreadPoolUtil.getFileHandlerExecutor();
if (executor == null)
{
// shouldn't normally happen, but just in case
return;
}
if (!this.beaconUpdateFuture.isDone())
{
return;
}
CompletableFuture<Void> future = new CompletableFuture<>();
this.beaconUpdateFuture = future;
try
{
executor.execute(() ->
{
this.refreshRenderingBeacons(playerPos);
try { Thread.sleep(2_000); } catch (InterruptedException ignore) { }
future.complete(null);
});
}
catch (RejectedExecutionException e)
{
// the thread pool was probably shut down because it's size is being changed, just wait a sec and it should be back
future.completeExceptionally(e);
}
}
private void refreshRenderingBeacons(DhBlockPos2D playerPos)
{
if (this.beaconBeamRepo == null
|| this.beaconRenderHandler == null)
{
return;
}
try
{
// Synchronized to prevent two threads for accessing the array at once
synchronized (this.beaconList)
{
// get beacons //
int blockDistanceRadius = (this.blockRenderDistanceDiameter / 2);
int minBlockPosX = playerPos.x - blockDistanceRadius;
int minBlockPosZ = playerPos.z - blockDistanceRadius;
int maxBlockPosX = playerPos.x + blockDistanceRadius;
int maxBlockPosZ = playerPos.z + blockDistanceRadius;
ArrayList<BeaconBeamDTO> dbBeacons = this.beaconBeamRepo.getAllBeamsInBlockPosRange(
minBlockPosX, maxBlockPosX,
minBlockPosZ, maxBlockPosZ
);
// convert DB beacons //
ArrayList<BeaconRenderHandler.BeaconBeamWithWidth> newBeaconList = new ArrayList<>(this.beaconList.size());
for (BeaconBeamDTO beaconBeam : dbBeacons)
{
byte beaconDetailLevel = this.calcExpectedDetailLevel(playerPos, beaconBeam.blockPos.getX(), beaconBeam.blockPos.getZ());
newBeaconList.add(new BeaconRenderHandler.BeaconBeamWithWidth(beaconBeam, beaconDetailLevel));
}
boolean replaceBeacons = false;
if (this.beaconList.size() != newBeaconList.size())
{
// lists are different sizes
replaceBeacons = true;
}
else
{
// sort the beacons so they can be compared
this.beaconList.sort(BeaconRenderHandler.NegativeInfiniteBlockPosComparator.INSTANCE);
newBeaconList.sort(BeaconRenderHandler.NegativeInfiniteBlockPosComparator.INSTANCE);
for (int i = 0; i < this.beaconList.size(); i++)
{
BeaconRenderHandler.BeaconBeamWithWidth oldBeam = this.beaconList.get(i);
BeaconRenderHandler.BeaconBeamWithWidth newBeam = newBeaconList.get(i);
if (!oldBeam.equals(newBeam))
{
replaceBeacons = true;
break;
}
}
}
// only replace the beacons if something changed
// this is done to prevent constantly re-uploading the render data
if (replaceBeacons)
{
this.beaconList.clear();
this.beaconList.addAll(newBeaconList);
this.beaconRenderHandler.replaceRenderingBeacons(this.beaconList);
}
}
}
catch (Exception e)
{
LOGGER.error("Unexpected issue updating beacons, error: ["+e.getMessage()+"].", e);
}
}
//endregion beacon handling
//====================//
// detail level logic //
//====================//
@@ -885,9 +1008,11 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
* @return detail level of this section pos
*/
public byte calcExpectedDetailLevel(DhBlockPos2D playerPos, long sectionPos)
{ return this.calcExpectedDetailLevel(playerPos, DhSectionPos.getCenterBlockPosX(sectionPos), DhSectionPos.getCenterBlockPosZ(sectionPos)); }
public byte calcExpectedDetailLevel(DhBlockPos2D playerPos, int targetBlockPosX, int targetBlockPosZ)
{
double blockDistance = playerPos.dist(DhSectionPos.getCenterBlockPosX(sectionPos), DhSectionPos.getCenterBlockPosZ(sectionPos));
return this.calcDetailLevelFromDistance(blockDistance);
double blockDistance = playerPos.dist(targetBlockPosX, targetBlockPosZ);
return this.calcDetailLevelFromDistance(blockDistance);
}
private void updateDetailLevelVariables()
@@ -73,21 +73,6 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
private final FullDataSourceProviderV2 fullDataSourceProvider;
private final LodQuadTree quadTree;
/**
* contains the list of beacons currently being rendered in this section
* if this list is modified the {@link LodRenderSection#beaconRenderHandler} should be updated to match.
*/
private final ArrayList<BeaconBeamDTO> activeBeaconList = new ArrayList<>();
@Nullable
public final BeaconRenderHandler beaconRenderHandler;
@Nullable
public final BeaconBeamRepo beaconBeamRepo;
/**
* locking is necessary to prevent some weird threading issues
* causing beacons to appear/disappear at the wrong times.
*/
private final ReentrantLock beaconRenderHandlingLock = new ReentrantLock();
private boolean renderingEnabled = false;
private boolean beaconsRendering = false;
@@ -134,9 +119,6 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
this.levelWrapper = level.getClientLevelWrapper();
this.fullDataSourceProvider = fullDataSourceProvider;
this.beaconRenderHandler = this.quadTree.beaconRenderHandler;
this.beaconBeamRepo = this.level.getBeaconBeamRepo();
DEBUG_RENDERER.register(this, Config.Client.Advanced.Debugging.DebugWireframe.showRenderSectionStatus);
}
@@ -188,8 +170,6 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
{
try
{
this.refreshActiveBeaconList();
LodQuadBuilder lodQuadBuilder = this.getAndBuildRenderData();
if (lodQuadBuilder == null)
{
@@ -376,130 +356,6 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
//=================//
// beacon handling //
//=================//
//region beacon handling
/** gets the active beacon list and stops/starts beacon rendering as necessary */
private void refreshActiveBeaconList()
{
try
{
this.beaconRenderHandlingLock.lock();
// do nothing if beacon rendering or repos are unavailable
if (this.beaconBeamRepo == null
|| this.beaconRenderHandler == null)
{
return;
}
// Synchronized to prevent two threads for accessing the array at once
synchronized (this.activeBeaconList)
{
ArrayList<BeaconBeamDTO> activeBeacons = this.beaconBeamRepo.getAllBeamsForPos(this.pos);
// swap old and new active beacon list
this.activeBeaconList.clear();
this.activeBeaconList.addAll(activeBeacons);
// if the beacons are currently rendering,
// re-create them so we can see any potential changes
if (this.beaconsRendering)
{
this.tryDisableBeacons();
this.tryEnableBeacons();
}
}
}
finally
{
this.beaconRenderHandlingLock.unlock();
}
}
public void tryDisableBeacons()
{
try
{
this.beaconRenderHandlingLock.lock();
// do nothing if beacon rendering is unavailable
if (this.beaconRenderHandler == null)
{
return;
}
if (!this.beaconsRendering)
{
return;
}
this.beaconsRendering = false;
if (Config.Client.Advanced.Debugging.DebugWireframe.showRenderSectionStatus.get())
{
// show that this position has just been disabled
DEBUG_RENDERER.makeParticle(
new AbstractDebugWireframeRenderer.BoxParticle(
new AbstractDebugWireframeRenderer.Box(this.pos, 128f, 156f, 0.09f, Color.CYAN.darker()),
0.2, 32f
)
);
}
synchronized (this.activeBeaconList)
{
this.beaconRenderHandler.stopRenderingBeaconsInRange(this.pos);
}
}
finally
{
this.beaconRenderHandlingLock.unlock();
}
}
public void tryEnableBeacons()
{
try
{
this.beaconRenderHandlingLock.lock();
// do nothing if beacon rendering is unavailable
if (this.beaconRenderHandler == null)
{
return;
}
if (this.beaconsRendering)
{
return;
}
this.beaconsRendering = true;
synchronized (this.activeBeaconList)
{
byte absoluteDetailLevel = (byte)(DhSectionPos.getDetailLevel(this.pos) - DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
this.beaconRenderHandler.startRenderingBeacons(this.activeBeaconList, absoluteDetailLevel);
}
}
finally
{
this.beaconRenderHandlingLock.unlock();
}
}
//endregion beacon handling
//==============//
// base methods //
//==============//
@@ -562,8 +418,6 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
}
this.tryDisableBeacons();
if (this.renderBufferContainer != null)
{
this.renderBufferContainer.close();
@@ -46,7 +46,6 @@ import java.util.*;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Predicate;
public class BeaconRenderHandler
{
@@ -57,8 +56,6 @@ public class BeaconRenderHandler
/** how often should we check if a beacon should be culled? */
private static final int MAX_CULLING_FREQUENCY_IN_MS = 1_000;
private static final Comparator<BeaconBeamDTO> NEGATIVE_BLOCKPOS_COMPARATOR = new NegativeInfiniteBlockPosComparator();
private final ReentrantLock updateLock = new ReentrantLock();
@@ -67,8 +64,6 @@ public class BeaconRenderHandler
private final IDhApiRenderableBoxGroup activeBeaconBoxRenderGroup;
/** contains all beacons that could be rendered (including those that are being culled) */
private final ArrayList<DhApiRenderableBox> fullBeaconBoxList = new ArrayList<>();
/** contains all beacons that could be rendered */
private final HashSet<DhBlockPos> fullBeaconBlockPosSet = new HashSet<>();
private boolean cullingThreadRunning = false;
private boolean updateRenderDataNextFrame = false;
@@ -96,126 +91,12 @@ public class BeaconRenderHandler
//=================//
// render handling //
//=================//
//===============//
// before render //
//===============//
//region
public void startRenderingBeacons(ArrayList<BeaconBeamDTO> beaconList, byte detailLevel)
{
try
{
this.updateLock.lock();
// how wide should each beacon be?
int beaconBlockWidth = 1;
if (Config.Client.Advanced.Graphics.GenericRendering.expandDistantBeacons.get())
{
beaconBlockWidth = DhSectionPos.getBlockWidth(detailLevel);
}
ArrayList<BeaconBeamDTO> sortedBeaconList = new ArrayList<>(beaconList);
// merge distant beams if requested
if (Config.Client.Advanced.Graphics.GenericRendering.expandDistantBeacons.get())
{
// sort beacons from neg inf -> pos inf
// so we can consistently merge adjacent beacons
sortedBeaconList.sort(NEGATIVE_BLOCKPOS_COMPARATOR);
// go through each beacon...
for (int outerIndex = 0; outerIndex < sortedBeaconList.size(); outerIndex++)
{
BeaconBeamDTO outerBeacon = sortedBeaconList.get(outerIndex);
DhBlockPos outerBlockPos = outerBeacon.blockPos;
// ...and remove any beacons that are within the block width to prevent overlaps
for (int mergeIndex = outerIndex + 1; mergeIndex < sortedBeaconList.size(); mergeIndex++)
{
BeaconBeamDTO beaconToMerge = sortedBeaconList.get(mergeIndex);
DhBlockPos mergeBlockPos = beaconToMerge.blockPos;
int xDiff = mergeBlockPos.getX() - outerBlockPos.getX();
int zDiff = mergeBlockPos.getZ() - outerBlockPos.getZ();
// merge (remove) this beacon if
// it's close to the outer beacon
if (xDiff < beaconBlockWidth
&& zDiff < beaconBlockWidth)
{
sortedBeaconList.remove(mergeIndex);
mergeIndex--; // minus 1 so we don't go past the end of the array when incrementing in the for loop up top
}
}
}
}
//LOGGER.info("startRenderingBeacons ["+sortedBeaconList+"]");
// add each beacon to the renderer
for (int i = 0; i < sortedBeaconList.size(); i++)
{
BeaconBeamDTO beacon = sortedBeaconList.get(i);
if (!this.fullBeaconBlockPosSet.add(beacon.blockPos))
{
// skip already present beacons
continue;
}
int maxBeaconBeamHeight = Config.Client.Advanced.Graphics.GenericRendering.beaconRenderHeight.get();
DhApiRenderableBox beaconBox = new DhApiRenderableBox(
new DhApiVec3d(beacon.blockPos.getX(), beacon.blockPos.getY() + 1, beacon.blockPos.getZ()),
new DhApiVec3d(beacon.blockPos.getX() + beaconBlockWidth, maxBeaconBeamHeight, beacon.blockPos.getZ() + beaconBlockWidth),
beacon.color,
EDhApiBlockMaterial.ILLUMINATED
);
this.activeBeaconBoxRenderGroup.add(beaconBox);
this.fullBeaconBoxList.add(beaconBox);
this.activeBeaconBoxRenderGroup.triggerBoxChange();
}
}
finally
{
this.updateLock.unlock();
}
}
public void stopRenderingBeaconsInRange(long pos)
{
try
{
this.updateLock.lock();
Predicate<DhApiRenderableBox> removeBoxPredicate = (DhApiRenderableBox box) ->
{
DhBlockPos blockPos = new DhBlockPos((int)box.minPos.x, (int)box.minPos.y, (int)box.minPos.z);
boolean contains = DhSectionPos.contains(pos, blockPos);
//if (contains)
//{
// LOGGER.info("stopRenderingBeaconsInRange ["+DhSectionPos.toString(pos)+"] ["+blockPos+"]");
//}
return contains;
};
this.activeBeaconBoxRenderGroup.removeIf(removeBoxPredicate);
this.fullBeaconBoxList.removeIf(removeBoxPredicate);
this.fullBeaconBlockPosSet.removeIf((DhBlockPos blockPos) -> DhSectionPos.contains(pos, blockPos));
this.activeBeaconBoxRenderGroup.triggerBoxChange();
}
finally
{
this.updateLock.unlock();
}
}
private void beforeRender(DhApiRenderParam renderEventParam)
private void beforeRender(DhApiRenderParam renderEventParam)
{
if (Config.Client.Advanced.Graphics.Culling.disableBeaconDistanceCulling.get())
{
@@ -237,7 +118,7 @@ public class BeaconRenderHandler
private void tryUpdateBeaconCullingAsync()
{
ThreadPoolExecutor executor = ThreadPoolUtil.getBeaconCullingExecutor();
if (executor != null
if (executor != null
&& !this.cullingThreadRunning)
{
this.cullingThreadRunning = true;
@@ -304,15 +185,136 @@ public class BeaconRenderHandler
//==============//
// registration //
//==============//
//region
public void replaceRenderingBeacons(ArrayList<BeaconBeamWithWidth> beaconList)
{
try
{
this.updateLock.lock();
ArrayList<BeaconBeamWithWidth> sortedBeaconList = new ArrayList<>(beaconList);
// merge distant beams if requested
if (Config.Client.Advanced.Graphics.GenericRendering.expandDistantBeacons.get())
{
// sort beacons from neg inf -> pos inf
// so we can consistently merge adjacent beacons
sortedBeaconList.sort(NegativeInfiniteBlockPosComparator.INSTANCE);
// go through each beacon...
for (int outerIndex = 0; outerIndex < sortedBeaconList.size(); outerIndex++)
{
BeaconBeamWithWidth outerBeacon = sortedBeaconList.get(outerIndex);
DhBlockPos outerBlockPos = outerBeacon.blockPos;
// ...and remove any beacons that are within the block width to prevent overlaps
for (int mergeIndex = outerIndex + 1; mergeIndex < sortedBeaconList.size(); mergeIndex++)
{
BeaconBeamWithWidth beaconToMerge = sortedBeaconList.get(mergeIndex);
DhBlockPos mergeBlockPos = beaconToMerge.blockPos;
int xDiff = mergeBlockPos.getX() - outerBlockPos.getX();
int zDiff = mergeBlockPos.getZ() - outerBlockPos.getZ();
// merge (remove) this beacon if
// it's close to the outer beacon
if (xDiff < beaconToMerge.beaconBlockWidth
&& zDiff < beaconToMerge.beaconBlockWidth)
{
sortedBeaconList.remove(mergeIndex);
mergeIndex--; // minus 1 so we don't go past the end of the array when incrementing in the for loop up top
}
}
}
}
this.activeBeaconBoxRenderGroup.clear();
this.fullBeaconBoxList.clear();
// add each beacon to the renderer
for (int i = 0; i < sortedBeaconList.size(); i++)
{
BeaconBeamWithWidth beacon = sortedBeaconList.get(i);
int maxBeaconBeamHeight = Config.Client.Advanced.Graphics.GenericRendering.beaconRenderHeight.get();
DhApiRenderableBox beaconBox = new DhApiRenderableBox(
new DhApiVec3d(beacon.blockPos.getX(), beacon.blockPos.getY() + 1, beacon.blockPos.getZ()),
new DhApiVec3d(beacon.blockPos.getX() + beacon.beaconBlockWidth, maxBeaconBeamHeight, beacon.blockPos.getZ() + beacon.beaconBlockWidth),
beacon.color,
EDhApiBlockMaterial.ILLUMINATED
);
this.activeBeaconBoxRenderGroup.add(beaconBox);
this.fullBeaconBoxList.add(beaconBox);
}
this.activeBeaconBoxRenderGroup.triggerBoxChange();
}
finally
{
this.updateLock.unlock();
}
}
//endregion
//================//
// helper classes //
//================//
//region
private static class NegativeInfiniteBlockPosComparator implements Comparator<BeaconBeamDTO>
public static class BeaconBeamWithWidth extends BeaconBeamDTO
{
public final int beaconBlockWidth;
public BeaconBeamWithWidth(BeaconBeamDTO beaconBeamDTO, byte lodDetailLevel)
{
super(beaconBeamDTO.blockPos, beaconBeamDTO.color);
// how wide should each beacon be?
if (Config.Client.Advanced.Graphics.GenericRendering.expandDistantBeacons.get())
{
this.beaconBlockWidth = DhSectionPos.getBlockWidth(lodDetailLevel);
}
else
{
this.beaconBlockWidth = 1;
}
}
@Override
public boolean equals(Object obj)
{
if (obj == null
|| obj.getClass() != this.getClass())
{
return false;
}
BeaconBeamWithWidth that = (BeaconBeamWithWidth) obj;
if (that.beaconBlockWidth != this.beaconBlockWidth)
{
return false;
}
return super.equals(that);
}
}
public static class NegativeInfiniteBlockPosComparator implements Comparator<BeaconBeamWithWidth>
{
public static final NegativeInfiniteBlockPosComparator INSTANCE = new NegativeInfiniteBlockPosComparator();
@Override
public int compare(BeaconBeamDTO beacon1, BeaconBeamDTO beacon2)
public int compare(BeaconBeamWithWidth beacon1, BeaconBeamWithWidth beacon2)
{
DhBlockPos blockPos1 = beacon1.blockPos;
DhBlockPos blockPos2 = beacon2.blockPos;
@@ -72,6 +72,20 @@ public class BeaconBeamDTO implements IBaseDTO<DhBlockPos>, INetworkObject
@Override
public DhBlockPos getKey() { return this.blockPos; }
@Override
public boolean equals(Object obj)
{
if (obj == null
|| obj.getClass() != this.getClass())
{
return false;
}
BeaconBeamDTO that = (BeaconBeamDTO)obj;
return this.blockPos.equals(that.blockPos)
&& this.color.equals(that.color);
}
@Override
public void close()
{ /* no closing needed */ }