Compare commits

..

79 Commits

Author SHA1 Message Date
James Seibel 9897570e6c Fully move getShade into the clientLevelWrapper 2026-05-31 07:43:05 -05:00
Acuadragon100 653b86c51a Move getShade into ClientLevelWrapper. 2026-05-31 11:15:34 +02:00
James Seibel 24d2fa90f4 fix world gen stopping when moving between dimensions 2026-05-30 17:53:50 -05:00
Acuadragon100 4f6d78189b Also use the original camera position when Immersive Portals is loaded. 2026-05-16 13:24:52 +02:00
Acuadragon100 1f7cf793c6 Merge remote-tracking branch 'root/main' into fix-portals 2026-05-15 16:42:23 +02:00
Acuadragon100 1908a0ccbf Fix null pointer exception when server level was unloaded before the client level. 2026-05-15 15:15:01 +02:00
James Seibel fd3a8f7ddf Add MC Version locking to the config 2026-05-15 07:44:00 -05:00
Acuadragon100 592b050937 Merge remote-tracking branch 'root/main' into fix-portals
# Conflicts:
#	core/src/main/java/com/seibel/distanthorizons/core/api/internal/ClientApi.java
#	core/src/main/java/com/seibel/distanthorizons/core/level/ClientLevelModule.java
2026-05-14 22:34:38 +02:00
Acuadragon100 9fa9b430f6 Only tick levels the player is in on the client. 2026-05-14 12:08:38 +02:00
Acuadragon100 c720a36f83 Always return the player's level and position even when a portal is being rendered. 2026-05-13 21:05:58 +02:00
James Seibel e3f586da56 temp comment out PooledDataSourceCheckoutTest 2026-05-12 21:56:18 -05:00
Acuadragon100 552ad226ea Avoid updating camera speed average when rendering a portal. 2026-05-12 21:21:18 +02:00
James Seibel 775984f651 add TODO 2026-05-11 22:01:00 -05:00
James Seibel b674f49600 up version number 3.0.3 -> 3.0.4 2026-05-04 07:41:32 -05:00
James Seibel b592012ba8 remove dev from version number 2026-05-03 18:20:22 -05:00
James Seibel 5d1e8a44fd up api version 6.1.0 -> 6.1.1 2026-05-03 18:20:12 -05:00
James Seibel 40b27335ea Add stack getting for render tasks 2026-05-03 16:45:23 -05:00
James Seibel d0b07a5d2f remove accidental debug code 2026-05-03 16:40:35 -05:00
James Seibel cb0fee9780 fix generic renderer buffer leak on level close 2026-05-03 16:36:32 -05:00
James Seibel 895e9276cd Fix GL buffer GC in RenderContainer canceling 2026-05-03 15:46:01 -05:00
James Seibel 9ee0af8b01 Add BasicPhantomReference for debugging 2026-05-03 15:45:52 -05:00
Acuadragon100 fd704bf8e6 Prevent multiple DhClientLevels of the same level from existing at once. 2026-05-03 21:07:03 +02:00
Acuadragon100 cc2febcb5c Fix current level unloading on the client. 2026-05-03 21:06:58 +02:00
Acuadragon100 809990f766 Make sure stale wrappers are not stored in the client level map. 2026-05-03 21:06:43 +02:00
James Seibel 69941fb7f8 DhApiBlockColorOverrideEvent use default alpha 2026-05-02 21:15:48 -05:00
James Seibel 36862a968f fix rare skylight application bug 2026-05-02 21:14:54 -05:00
James Seibel 27204336b2 cleanup lod buffer container closing 2026-05-02 21:14:14 -05:00
James Seibel 4846cf5019 comment out unnecessary shutdown logging 2026-05-02 21:13:07 -05:00
James Seibel f7f3c1146f separate shared phantom logging logic 2026-05-02 21:12:26 -05:00
James Seibel aaa5e958f0 Fix LOD shading applying incorrectly with Iris 2026-05-02 15:14:25 -05:00
James Seibel 726da953bd Merge branch 'distant-horizons-core-optimizations' 2026-05-02 11:35:26 -05:00
James Seibel c4f4935fdd Remove unused mac render code 2026-05-02 10:36:44 -05:00
James Seibel 5cb30ed7ce disable immersive portals cam speed calculation 2026-05-02 09:56:28 -05:00
James Seibel 5f54ad0650 up net protocol version 13 -> 14 2026-05-02 09:55:57 -05:00
James Seibel 9fc4d840fc immersive portal quad tree player pos fix 2026-05-02 09:55:45 -05:00
James Seibel 52b0acc452 Immersive Portal Accessor refactoring 2026-05-02 09:55:00 -05:00
James Seibel 4e647395e8 minor format updating 2026-05-02 09:53:02 -05:00
James Seibel 3ef8bd7e20 Add position finder debug config 2026-04-29 07:35:16 -05:00
James Seibel ec72762067 use camera pos for detail calculations 2026-04-28 07:09:22 -05:00
James Seibel 4d0ed2a6dc fix null pointer on dedicated server shutdown 2026-04-27 07:48:06 -05:00
James Seibel 7b252b173b Fix wyncraft getting stuck at low LOD quality 2026-04-27 07:27:03 -05:00
Acuadragon100 25ac1de59b Check if same level before trying to decode the data. 2026-04-26 13:25:00 +02:00
Acuadragon100 949124f8dc Fix not unloading client levels on servers. 2026-04-26 13:24:55 +02:00
Acuadragon100 c363b7fe4b Check fade rendering even without Sodium, because it can still happen without Sodium in some cases. 2026-04-26 13:24:53 +02:00
Acuadragon100 dcb049d4c2 Disable fading detection on 1.21.6+ for now.
Might be worth revisiting once a proper fork of Immersive Portals appear for newer versions.
2026-04-26 13:24:51 +02:00
Acuadragon100 ea51b9135d Import cleanup 2026-04-26 13:24:48 +02:00
Acuadragon100 da31547cfc Allow client to load all dimensions. 2026-04-26 13:24:46 +02:00
Acuadragon100 00f9fd8e53 Allow updating other dimensions. 2026-04-26 13:24:44 +02:00
Acuadragon100 7149baf0f6 Fix server loading. 2026-04-26 13:24:42 +02:00
Acuadragon100 ef3e7763dc Redo loading 2026-04-26 13:24:39 +02:00
Acuadragon100 f5ac5c56b4 Looks like shouldSkipRenderingPortal is sometimes not static. 2026-04-26 13:24:37 +02:00
Acuadragon100 77f10bed48 Probably want to detect rubidium and embeddium as well. 2026-04-26 13:24:35 +02:00
Acuadragon100 7fe0c9b0e8 Tweaked portal loading and fix portals not being detected to disable fading until entered at least once. 2026-04-26 13:24:32 +02:00
Acuadragon100 3d13ba7645 Disable fade rendering when immersive portals and sodium are active at once. 2026-04-26 13:24:13 +02:00
James Seibel 7b0c66e3ae up version number 3.0.2 -> 3.0.3 2026-04-24 06:51:39 -05:00
James Seibel 1b066327a8 remove dev from the version number 2026-04-24 06:50:47 -05:00
James Seibel 43d0a971f7 add todo commented code 2026-04-24 06:50:00 -05:00
James Seibel 9e60c698de move before render pass events into render api 2026-04-23 17:54:33 -05:00
James Seibel bf2affa6d1 Fix "fog" rendering when underwater with Iris 2026-04-23 17:39:40 -05:00
James Seibel 98f6cea86a Fix near clip plane to close with shaders 2026-04-23 17:09:24 -05:00
James Seibel 9ae01dc1f8 up api version 6.0.0 -> 6.1.0 2026-04-23 07:42:21 -05:00
James Seibel 40efc5cbf3 Add alpha to DhApiBlockColorOverrideEvent 2026-04-23 07:42:07 -05:00
James Seibel 66bba1c80a add opacity to API block state wrapper 2026-04-23 07:41:51 -05:00
James Seibel d9f3b31cc5 Add timeout to CSV block culling configs 2026-04-22 18:48:21 -05:00
James Seibel e465ef5325 Fix flashing when moving over root node boundaries 2026-04-22 18:36:09 -05:00
s809 225385a43f Clean up received payload buffer check a bit 2026-04-23 00:26:32 +05:00
James Seibel 7d7d07416b Fix quad tree unit tests 2026-04-22 07:41:44 -05:00
James Seibel 5ef308cbee fix rare race condition preventing world gen 2026-04-21 22:19:57 -05:00
James Seibel d61b601c14 fix potential exceptions after world shutdown 2026-04-21 22:19:46 -05:00
James Seibel 246c679a97 Maybe fix native GL crash due to buffer free 2026-04-21 21:40:20 -05:00
James Seibel 4b317a8e00 Fix garbage collector warning not using config 2026-04-21 19:59:17 -05:00
James Seibel 1debd4b875 Improve node out-of-bound logic
This fixes some overlapping rendering issues, fixes LOD generating outside of render distance, and fixes low-detail LODs flashing when moving into previously-explored LODs
2026-04-21 19:49:50 -05:00
James Seibel 5dcda31990 Try fixing LOD flashing/stuck low details 2026-04-21 07:48:07 -05:00
James Seibel ae16ed2341 Revert "Fix LODs loading outside render distance"
This reverts commit 2c266d2495.
2026-04-20 21:32:00 -05:00
James Seibel 2c266d2495 Fix LODs loading outside render distance
Fixes !1233
2026-04-19 21:48:26 -05:00
James Seibel 7e40546bc5 fix world gen not canceling for far away pos 2026-04-19 21:10:20 -05:00
James Seibel 5d391c83ea quad tree region/comment cleanup 2026-04-19 21:07:42 -05:00
James Seibel 0895bf53e3 renderDataPointUtil toString cleanup 2026-04-18 21:45:24 -05:00
James Seibel a7203f8f33 up version number 3.0.1 -> 3.0.2 2026-04-18 21:44:17 -05:00
65 changed files with 2136 additions and 924 deletions
@@ -42,6 +42,12 @@ public interface IDhApiBlockStateWrapper extends IDhApiUnsafeWrapper
/** @since API 1.0.0 */
boolean isLiquid();
/**
* Returns a value between 0 (fully transparent) and 16 (fully opaque).
* @since 6.1.0
*/
int getOpacity();
/**
* Returns the full serialized form of the given block
* as defined by DH's serialization methods.
@@ -107,16 +107,24 @@ public abstract class DhApiBlockColorOverrideEvent implements IDhApiEvent<DhApiB
public IDhApiLevelWrapper getLevelWrapper() { return levelWrapper; }
public int getColorAsInt() { return this.colorAsInt; }
public int getAlpha() { return ColorUtil.getAlpha(this.colorAsInt); }
public int getRed() { return ColorUtil.getRed(this.colorAsInt); }
public int getGreen() { return ColorUtil.getGreen(this.colorAsInt); }
public int getBlue() { return ColorUtil.getBlue(this.colorAsInt); }
public void setColor(int red, int green, int blue) throws IllegalArgumentException
public void setColor(int red, int green, int blue) throws IllegalArgumentException { this.setColor(this.getAlpha(), red, green, blue); }
/**
* Note: when if you set a partially transparent alpha channel the underlying {@link IDhApiBlockStateWrapper#getOpacity()}
* method should also return a non-opaque value.
* Otherwise LODs may behave incorrectly.
*/
public void setColor(int alpha, int red, int green, int blue) throws IllegalArgumentException
{
ColorUtil.throwIfColorValueOutOfIntRange("alpha", alpha);
ColorUtil.throwIfColorValueOutOfIntRange("red", red);
ColorUtil.throwIfColorValueOutOfIntRange("green", green);
ColorUtil.throwIfColorValueOutOfIntRange("blue", blue);
this.colorAsInt = ColorUtil.rgbToInt(red, green, blue);
this.colorAsInt = ColorUtil.argbToInt(alpha, red, green, blue);
}
/** @return the block's X value in the world */
@@ -35,7 +35,7 @@ import java.util.Map;
*/
public class DependencyInjector<BindableType extends IBindable> implements IDependencyInjector<BindableType> // Note to self: Don't try adding a generic type to IDhApiEvent, the constructor won't accept it
{
protected final Map<Class<? extends BindableType>, ArrayList<BindableType>> dependencies = new HashMap<>();
protected final HashMap<Class<? extends BindableType>, ArrayList<BindableType>> dependencies = new HashMap<>();
/** Internal class reference to BindableType since we can't get it any other way. */
protected final Class<? extends BindableType> bindableInterface;
@@ -31,7 +31,7 @@ public final class ModInfo
public static final String DEDICATED_SERVER_INITIAL_PATH = "dedicated_server_initial";
/** Incremented every time any packets are added, changed or removed, with a few exceptions. */
public static final int PROTOCOL_VERSION = 13;
public static final int PROTOCOL_VERSION = 14;
/**
* The full plugin channel name (RESOURCE_NAMESPACE:WRAPPER_PACKET_PATH)
@@ -43,16 +43,16 @@ public final class ModInfo
public static final String NAME = "DistantHorizons";
/** Human-readable version of NAME */
public static final String READABLE_NAME = "Distant Horizons";
public static final String VERSION = "3.0.1-b";
public static final String VERSION = "3.0.4-b-dev";
/** Returns true if the current build is an unstable developer build, false otherwise. */
public static final boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev");
/** This version should only be updated when breaking changes are introduced to the DH API */
public static final int API_MAJOR_VERSION = 6;
/** This version should be updated whenever new methods are added to the DH API */
public static final int API_MINOR_VERSION = 0;
public static final int API_MINOR_VERSION = 1;
/** This version should be updated whenever non-breaking fixes are added to the DH API */
public static final int API_PATCH_VERSION = 0;
public static final int API_PATCH_VERSION = 1;
/** If the config file has an older version it'll be re-created from scratch. */
public static final int CONFIG_FILE_VERSION = 4;
@@ -54,7 +54,7 @@ public class Initializer
public static void init()
public static void preConfigInit()
{
//============================//
// check referenced libraries //
@@ -177,6 +177,11 @@ public class Initializer
//endregion
}
/** fired after DH's config has been set up */
public static void postConfigInit()
{
//==============================//
@@ -238,8 +243,7 @@ public class Initializer
//endregion
}
}
@@ -24,6 +24,7 @@ import com.seibel.distanthorizons.api.enums.config.EDhApiMcRenderingFadeMode;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.*;
import com.seibel.distanthorizons.core.api.internal.rendering.DhRenderState;
import com.seibel.distanthorizons.core.dependencyInjection.ModAccessorInjector;
import com.seibel.distanthorizons.core.enums.MinecraftTextFormat;
import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
@@ -40,6 +41,8 @@ import com.seibel.distanthorizons.core.util.objects.Pair;
import com.seibel.distanthorizons.core.util.objects.RollingAverage;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor.IImmersivePortalsAccessor;
import com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor.IIrisAccessor;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.renderPass.IDhMetaRenderer;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.renderPass.IDhVanillaFadeRenderer;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.renderPass.IDhTestTriangleRenderer;
@@ -51,7 +54,6 @@ import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiDebugRendering;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiRendererMode;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IServerKeyedClientLevel;
import com.seibel.distanthorizons.core.world.AbstractDhWorld;
import com.seibel.distanthorizons.core.world.DhClientWorld;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
@@ -63,8 +65,10 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.lwjgl.glfw.GLFW;
import java.awt.*;
import java.io.File;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadPoolExecutor;
@@ -84,6 +88,11 @@ public class ClientApi
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
// Need to classload this field later because otherwise it will be null even when Immersive Portals is present.
public static class Late {
private static final IImmersivePortalsAccessor IMMERSIVE_PORTALS = ModAccessorInjector.INSTANCE.get(IImmersivePortalsAccessor.class);
}
/** this includes the is dev build message and low allocated memory warning */
private static final int MS_BETWEEN_STATIC_STARTUP_MESSAGES = 4_000;
@@ -116,7 +125,7 @@ public class ClientApi
public boolean rendererDisabledBecauseOfExceptions = false;
private final ClientPluginChannelApi pluginChannelApi = new ClientPluginChannelApi(this::clientLevelLoadEvent, this::clientLevelUnloadEvent);
private final ClientPluginChannelApi pluginChannelApi = new ClientPluginChannelApi();
/** Delay loading the first level to give the server some time to respond with level to actually load */
private Timer firstLevelLoadTimer;
@@ -124,8 +133,8 @@ public class ClientApi
/** Holds any levels that were loaded before the {@link ClientApi#onClientOnlyConnected} was fired. */
public final HashSet<IClientLevelWrapper> waitingClientLevels = new HashSet<>();
/** Holds any chunks that were loaded before the {@link ClientApi#clientLevelLoadEvent(IClientLevelWrapper)} was fired. */
public final HashMap<Pair<IClientLevelWrapper, DhChunkPos>, IChunkWrapper> waitingChunkByClientLevelAndPos = new HashMap<>();
/** Holds any chunks that were found before the client levels are loaded. */
public final Map<Pair<IClientLevelWrapper, DhChunkPos>, IChunkWrapper> waitingChunkByClientLevelAndPos = new ConcurrentHashMap<>();
/** publicly available so {@link F3Screen} can display the error */
@Nullable
@@ -141,12 +150,25 @@ public class ClientApi
* tracked should also be to keep the ratio roughly the same.
* @see ClientApi#MIN_MS_BETWEEN_SPEED_CHECKS
*/
public RollingAverage cameraSpeedRollingAverage = new RollingAverage(40);
private RollingAverage cameraSpeedRollingAverage = new RollingAverage(40);
private Vec3d lastCameraPosForSpeedCheck = new Vec3d();
private long msSinceLastSpeedCheck = 0L;
public double getAvgCameraSpeed()
{
return cameraSpeedRollingAverage.getAverage();
}
public static long firstRenderTimeMs = 0;
/**
* keeping track of this is necessary to fix
* out-of-date LODs from rendering when the shading
* is changed by Iris, causing LODs to often
* lack the side shading, which looks pretty bad
* when shaders are disabled.
*/
private boolean irisShadersEnabledLastFrame = false;
//==============//
@@ -160,11 +182,11 @@ public class ClientApi
//==============//
// world events //
//==============//
//region
//region world events
/**
* May be fired slightly before or after the associated
* {@link ClientApi#clientLevelLoadEvent(IClientLevelWrapper)} event
* level is loaded
* depending on how the host mod loader functions. <br><br>
*
* Synchronized shouldn't be necessary, but is present to match {@see onClientOnlyDisconnected} and prevent any unforeseen issues.
@@ -201,14 +223,6 @@ public class ClientApi
this.pluginChannelApi.onJoinServer(world.networkState.getSession());
world.networkState.sendConfigMessage();
LOGGER.info("Loading [" + this.waitingClientLevels.size() + "] waiting client level wrappers.");
for (IClientLevelWrapper level : this.waitingClientLevels)
{
this.clientLevelLoadEvent(level);
}
this.waitingClientLevels.clear();
}
}
@@ -235,7 +249,55 @@ public class ClientApi
// remove any waiting items
this.waitingChunkByClientLevelAndPos.clear();
this.waitingClientLevels.clear();
}
//endregion
//==============//
// level events //
//==============//
//region level events
/**
* used in conjunction with the server networking to
* handle level load requests.
*/
public boolean canLoadClientLevel(IClientLevelWrapper wrapper)
{
// wait a moment before loading the level to give the server a chance to handle the client's login request
if (MC_CLIENT.clientConnectedToDedicatedServer())
{
if (this.firstLevelLoadTimer == null)
{
this.firstLevelLoadTimer = TimerUtil.CreateTimer("FirstLevelLoadTimer");
this.firstLevelLoadTimer.schedule(new TimerTask()
{
@Override
public void run() { canLoadClientLevel(wrapper); }
}, FIRST_LEVEL_LOAD_DELAY_IN_MS);
return false;
}
this.firstLevelLoadTimer.cancel();
}
if (!this.pluginChannelApi.allowLevelLoading(wrapper))
{
LOGGER.debug("Client levels in this connection are managed by the server, skipping auto-load of: ["+wrapper+"]");
AbstractDhWorld world = SharedApi.getAbstractDhWorld();
if (world == null)
{
return false;
}
// Instead of attempting to load themselves, send the config and wait for a server provided level key.
((DhClientWorld) world).networkState.sendLevelInitRequest(wrapper.getDimensionName());
return false;
}
return true;
}
//endregion
@@ -247,95 +309,7 @@ public class ClientApi
//==============//
//region
public void clientLevelUnloadEvent(IClientLevelWrapper level)
{
try
{
LOGGER.info("Unloading client level [" + level.getClass().getSimpleName() + "]-[" + level.getDhIdentifier() + "].");
if (level instanceof IServerKeyedClientLevel)
{
this.pluginChannelApi.onClientLevelUnload();
}
AbstractDhWorld world = SharedApi.getAbstractDhWorld();
if (world != null)
{
world.unloadLevel(level);
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(level));
}
else
{
this.waitingClientLevels.remove(level);
}
}
catch (Exception e)
{
// handle errors here to prevent blowing up a mixin or API up stream
LOGGER.error("Unexpected error in ClientApi.clientLevelUnloadEvent(), error: "+e.getMessage(), e);
}
}
public void clientLevelLoadEvent(@Nullable IClientLevelWrapper levelWrapper)
{
// can happen if there was an issue during level load
if (levelWrapper == null)
{
return;
}
// wait a moment before loading the level to give the server a chance to handle the client's login request
if (MC_CLIENT.clientConnectedToDedicatedServer())
{
if (this.firstLevelLoadTimer == null)
{
this.firstLevelLoadTimer = TimerUtil.CreateTimer("FirstLevelLoadTimer");
this.firstLevelLoadTimer.schedule(new TimerTask()
{
@Override
public void run() { ClientApi.this.clientLevelLoadEvent(levelWrapper); }
}, FIRST_LEVEL_LOAD_DELAY_IN_MS);
return;
}
this.firstLevelLoadTimer.cancel();
}
try
{
LOGGER.info("Loading client level [" + levelWrapper + "]-[" + levelWrapper.getDhIdentifier() + "].");
AbstractDhWorld world = SharedApi.getAbstractDhWorld();
if (world != null)
{
if (!this.pluginChannelApi.allowLevelLoading(levelWrapper))
{
LOGGER.info("Levels in this connection are managed by the server, skipping auto-load.");
// Instead of attempting to load themselves, send the config and wait for a server provided level key.
((DhClientWorld) world).networkState.sendConfigMessage();
return;
}
world.getOrLoadLevel(levelWrapper);
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(levelWrapper));
this.loadWaitingChunksForLevel(levelWrapper);
}
else
{
this.waitingClientLevels.add(levelWrapper);
}
}
catch (Exception e)
{
// handle errors here to prevent blowing up a mixin or API up stream
LOGGER.error("Unexpected error in ClientApi.clientLevelLoadEvent(), error: "+e.getMessage(), e);
}
}
private void loadWaitingChunksForLevel(IClientLevelWrapper level)
public void loadWaitingChunksForLevel(IClientLevelWrapper level)
{
HashSet<Pair<IClientLevelWrapper, DhChunkPos>> keysToRemove = new HashSet<>();
for (Pair<IClientLevelWrapper, DhChunkPos> levelChunkPair : this.waitingChunkByClientLevelAndPos.keySet())
@@ -364,7 +338,7 @@ public class ClientApi
//============//
// networking //
//============//
//region
//region networking
/**
* Forwards a decoded message into the registered handlers.
@@ -404,7 +378,7 @@ public class ClientApi
//===============//
// LOD rendering //
//===============//
//region
//region lod rendering
/** Should be called before {@link ClientApi#renderDeferredLodsForShaders} */
public void renderLods() { this.renderLodLayer(false); }
@@ -428,12 +402,16 @@ public class ClientApi
//===========//
//region
//DhApiTerrainDataRepo.asyncDebugMethod(
// RENDER_STATE.clientLevelWrapper,
// MC_CLIENT.getPlayerBlockPos().getX(),
// MC_CLIENT.getPlayerBlockPos().getY(),
// MC_CLIENT.getPlayerBlockPos().getZ()
//);
// only run these tasks once per frame
if (!renderingDeferredLayer)
{
//DhApiTerrainDataRepo.asyncDebugMethod(
// RENDER_STATE.clientLevelWrapper,
// MC_CLIENT.getPlayerBlockPos().getX(),
// MC_CLIENT.getPlayerBlockPos().getY(),
// MC_CLIENT.getPlayerBlockPos().getZ()
//);
}
//endregion
@@ -482,7 +460,7 @@ public class ClientApi
//region
long nowMs = System.currentTimeMillis();
if (this.msSinceLastSpeedCheck + MIN_MS_BETWEEN_SPEED_CHECKS < nowMs)
if (this.msSinceLastSpeedCheck + MIN_MS_BETWEEN_SPEED_CHECKS < nowMs && (Late.IMMERSIVE_PORTALS == null || !Late.IMMERSIVE_PORTALS.isRenderingPortal()))
{
// calc time since last check
double secSinceLastCheck = (nowMs - this.msSinceLastSpeedCheck) / 1_000.0;
@@ -499,6 +477,27 @@ public class ClientApi
}
//endregion
//====================//
// Iris data re-build //
//====================//
//region
// delayed getter since ClientApi is created before this accessor is bound
IIrisAccessor irisAccessor = ModAccessorInjector.INSTANCE.get(IIrisAccessor.class);
if (irisAccessor != null)
{
boolean shadersActive = irisAccessor.isShaderPackInUse();
if (this.irisShadersEnabledLastFrame != shadersActive)
{
this.irisShadersEnabledLastFrame = shadersActive;
DhApi.Delayed.renderProxy.clearRenderDataCache();
}
}
//endregion
}
}
@@ -667,7 +666,7 @@ public class ClientApi
//================//
// fade rendering //
//================//
//region
//region fade rendering
/**
* The first fade pass.
@@ -690,8 +689,7 @@ public class ClientApi
// or if LOD-only mode is enabled (fading is used to remove the MC render pass)
|| Config.Client.Advanced.Debugging.lodOnlyMode.get()
)
// don't fade when Iris shaders are active, otherwise the rendering can get weird
&& !DhApiRenderProxy.INSTANCE.getDeferTransparentRendering())
&& shouldRenderFade())
{
RenderParams renderParams = new RenderParams(EDhApiRenderPass.OPAQUE, RENDER_STATE);
fadeRenderer.render(renderParams);
@@ -720,8 +718,7 @@ public class ClientApi
// or if LOD-only mode is enabled (fading is used to remove the MC render pass)
|| Config.Client.Advanced.Debugging.lodOnlyMode.get()
)
// don't fade when Iris shaders are active, otherwise the rendering can get weird
&& !DhApiRenderProxy.INSTANCE.getDeferTransparentRendering();
&& shouldRenderFade();
if (renderFade)
{
RenderParams renderParams = new RenderParams(EDhApiRenderPass.TRANSPARENT, RENDER_STATE);
@@ -730,6 +727,25 @@ public class ClientApi
}
}
private static boolean shouldRenderFade()
{
// don't fade when Iris shaders are active, otherwise the rendering can get weird
if (DhApiRenderProxy.INSTANCE.getDeferTransparentRendering())
{
return false;
}
// When immersive portals and sodium are combined the fade renders on top of the portal, so turn it off when a portal is on-screen.
IImmersivePortalsAccessor immersivePortals = ModAccessorInjector.INSTANCE.get(IImmersivePortalsAccessor.class);
if (immersivePortals != null
&& immersivePortals.wasPortalRecentlyVisible())
{
return false;
}
return true;
}
//endregion
@@ -737,7 +753,7 @@ public class ClientApi
//==========//
// keyboard //
//==========//
//region
//region keyboard
/** Trigger once on key press, with CLIENT PLAYER. */
public void keyPressedEvent(int glfwKey)
@@ -773,7 +789,7 @@ public class ClientApi
//======//
// chat //
//======//
//region
//region chat
private void sendQueuedChatMessages()
{
@@ -10,6 +10,7 @@ import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent
import com.seibel.distanthorizons.core.network.messages.base.LevelInitMessage;
import com.seibel.distanthorizons.core.network.session.NetworkSession;
import com.seibel.distanthorizons.core.render.RenderThreadTaskHandler;
import com.seibel.distanthorizons.core.world.AbstractDhWorld;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import org.jetbrains.annotations.NotNull;
@@ -30,9 +31,6 @@ public class ClientPluginChannelApi
private static final IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IKeyedClientLevelManager KEYED_CLIENT_LEVEL_MANAGER = SingletonInjector.INSTANCE.get(IKeyedClientLevelManager.class);
private final Consumer<IServerKeyedClientLevel> levelLoadHandler;
private final Consumer<IClientLevelWrapper> levelUnloadHandler;
@Nullable
public NetworkSession networkSession;
@@ -42,10 +40,8 @@ public class ClientPluginChannelApi
// constructor //
//=============//
public ClientPluginChannelApi(Consumer<IServerKeyedClientLevel> levelLoadHandler, Consumer<IClientLevelWrapper> levelUnloadHandler)
public ClientPluginChannelApi()
{
this.levelLoadHandler = levelLoadHandler;
this.levelUnloadHandler = levelUnloadHandler;
}
@@ -94,24 +90,6 @@ public class ClientPluginChannelApi
{
IClientLevelWrapper clientLevel = MC.getWrappedClientLevel(true);
IServerKeyedClientLevel existingKeyedClientLevel = KEYED_CLIENT_LEVEL_MANAGER.getServerKeyedLevel();
if (existingKeyedClientLevel != null)
{
if (!existingKeyedClientLevel.getServerLevelKey().equals(msg.levelKey))
{
LOGGER.info("Unloading previous level with key: [" + existingKeyedClientLevel.getServerLevelKey() + "].");
this.levelUnloadHandler.accept(existingKeyedClientLevel);
}
else
{
LOGGER.info("Level key matches the previous level key, ignoring the message.");
}
}
else
{
LOGGER.info("Unloading non-keyed level: [" + clientLevel.getDhIdentifier() + "].");
this.levelUnloadHandler.accept(clientLevel);
}
if (existingKeyedClientLevel == null
|| !existingKeyedClientLevel.getServerKey().equals(msg.serverKey)
@@ -119,7 +97,11 @@ public class ClientPluginChannelApi
{
LOGGER.info("Loading level with key: [" + msg.levelKey + "].");
IServerKeyedClientLevel keyedLevel = KEYED_CLIENT_LEVEL_MANAGER.setServerKeyedLevel(clientLevel, msg.serverKey, msg.levelKey);
this.levelLoadHandler.accept(keyedLevel);
AbstractDhWorld world = SharedApi.getAbstractDhWorld();
if (world != null)
{
world.getOrLoadLevel(keyedLevel);
}
}
});
}
@@ -19,13 +19,10 @@
package com.seibel.distanthorizons.core.api.internal;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelLoadEvent;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelUnloadEvent;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import com.seibel.distanthorizons.core.network.messages.MessageRegistry;
import com.seibel.distanthorizons.core.world.*;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
@@ -77,7 +74,6 @@ public class ServerApi
}
//==============//
// level events //
//==============//
@@ -90,7 +86,6 @@ public class ServerApi
if (serverWorld != null)
{
serverWorld.getOrLoadLevel(levelWrapper);
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(levelWrapper));
}
}
public void serverLevelUnloadEvent(IServerLevelWrapper level)
@@ -101,12 +96,10 @@ public class ServerApi
if (serverWorld != null)
{
serverWorld.unloadLevel(level);
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(level));
}
}
//=======================//
// chunk modified events //
//=======================//
@@ -122,7 +115,7 @@ public class ServerApi
public void serverPlayerJoinEvent(IServerPlayerWrapper player)
{
if (DhApiWorldProxy.INSTANCE.worldLoaded() && DhApiWorldProxy.INSTANCE.getReadOnly())
if (DhApiWorldProxy.INSTANCE.tryGetReadOnly())
{
return;
}
@@ -136,7 +129,7 @@ public class ServerApi
}
public void serverPlayerDisconnectEvent(IServerPlayerWrapper player)
{
if (DhApiWorldProxy.INSTANCE.worldLoaded() && DhApiWorldProxy.INSTANCE.getReadOnly())
if (DhApiWorldProxy.INSTANCE.tryGetReadOnly())
{
return;
}
@@ -150,7 +143,7 @@ public class ServerApi
}
public void serverPlayerLevelChangeEvent(IServerPlayerWrapper player, IServerLevelWrapper originLevel, IServerLevelWrapper destinationLevel)
{
if (DhApiWorldProxy.INSTANCE.worldLoaded() && DhApiWorldProxy.INSTANCE.getReadOnly())
if (DhApiWorldProxy.INSTANCE.tryGetReadOnly())
{
return;
}
@@ -170,7 +163,7 @@ public class ServerApi
*/
public void pluginMessageReceived(IServerPlayerWrapper player, @NotNull AbstractNetworkMessage message)
{
if (DhApiWorldProxy.INSTANCE.worldLoaded() && DhApiWorldProxy.INSTANCE.getReadOnly())
if (DhApiWorldProxy.INSTANCE.tryGetReadOnly())
{
return;
}
@@ -74,7 +74,6 @@ public class SharedApi
//region
private SharedApi() { }
public static void init() { Initializer.init(); }
//endregion
@@ -212,7 +211,7 @@ public class SharedApi
}
// ignore updates if the world is read-only
if (DhApiWorldProxy.INSTANCE.getReadOnly())
if (DhApiWorldProxy.INSTANCE.tryGetReadOnly())
{
return;
}
@@ -964,6 +964,7 @@ public class Config
public static ConfigCategory debugWireframe = new ConfigCategory.Builder().set(DebugWireframe.class).build();
public static ConfigCategory openGl = new ConfigCategory.Builder().set(OpenGl.class).build();
public static ConfigCategory columnBuilderDebugging = new ConfigCategory.Builder().set(ColumnBuilderDebugging.class).build();
public static ConfigCategory positionFinderDebugging = new ConfigCategory.Builder().set(PositionFinder.class).build();
public static ConfigCategory f3Screen = new ConfigCategory.Builder().set(F3Screen.class).build();
public static ConfigCategory exampleConfigScreen = new ConfigCategory.Builder().set(ExampleConfigScreen.class).build();
@@ -1095,6 +1096,36 @@ public class Config
}
public static class PositionFinder
{
//public static ConfigUIComment positionFinderHeader = new ConfigUIComment.Builder().setParentConfigClass(ColumnBuilderDebugging.class).build();
public static ConfigEntry<Boolean> positionFinderEnable = new ConfigEntry.Builder<Boolean>()
.set(false)
.build();
public static ConfigEntry<Integer> positionFinderDetailLevel = new ConfigEntry.Builder<Integer>()
.set((int) DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL)
.build();
public static ConfigEntry<Integer> positionFinderXPos = new ConfigEntry.Builder<Integer>()
.set(0)
.build();
public static ConfigEntry<Integer> positionFinderZPos = new ConfigEntry.Builder<Integer>()
.set(0)
.build();
public static ConfigEntry<Integer> positionFinderMinBlockY = new ConfigEntry.Builder<Integer>()
.set(-64)
.build();
public static ConfigEntry<Integer> positionFinderMaxBlockY = new ConfigEntry.Builder<Integer>()
.set(125)
.build();
public static ConfigEntry<Float> positionFinderMarginPercent = new ConfigEntry.Builder<Float>()
.set(0.0f)
.build();
}
public static class F3Screen
{
public static ConfigUIComment f3ScreenHeader = new ConfigUIComment.Builder().setParentConfigClass(F3Screen.class).build();
@@ -0,0 +1,84 @@
package com.seibel.distanthorizons.core.config.eventHandlers;
import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
import com.seibel.distanthorizons.core.util.TimerUtil;
import java.util.Timer;
import java.util.TimerTask;
public abstract class AbstractDelayedConfigEventHandler implements IConfigListener
{
public static final long DEFAULT_TIMEOUT_IN_MS = 2_000L;
/** how long to wait in milliseconds before applying the config changes */
private final long timeoutInMs;
private Timer timer;
//=============//
// constructor //
//=============//
//region
public AbstractDelayedConfigEventHandler(long timeoutInMs) { this.timeoutInMs = timeoutInMs; }
//endregion
//==================//
// abstract methods //
//==================//
//region
public abstract void onConfigTimeout();
//endregion
//========//
// events //
//========//
//region
@Override
public void onConfigValueSet()
{
if (this.timeoutInMs > 0)
{
this.refreshRenderDataAfterTimeout();
}
else
{
this.onConfigTimeout();
}
}
/** Calling this method multiple times will reset the timer */
private synchronized void refreshRenderDataAfterTimeout() // synchronized to prevent potential threading issues when adding/removing the timer
{
// stop the previous timer if one exists
if (this.timer != null)
{
this.timer.cancel();
}
// create a new timer task
TimerTask timerTask = new TimerTask()
{
public void run()
{
AbstractDelayedConfigEventHandler.this.onConfigTimeout();
}
};
this.timer = TimerUtil.CreateTimer("AbstractDelayedConfigTimer");
this.timer.schedule(timerTask, this.timeoutInMs);
}
//endregion
}
@@ -27,72 +27,36 @@ import com.seibel.distanthorizons.core.util.TimerUtil;
import java.util.Timer;
import java.util.TimerTask;
public class ReloadLodsConfigEventHandler implements IConfigListener
public class ReloadLodsConfigEventHandler extends AbstractDelayedConfigEventHandler
{
/**
* should be used for user facing UI options
* this allows the user a second to click through options before they're applied
*/
public static ReloadLodsConfigEventHandler DELAYED_INSTANCE = new ReloadLodsConfigEventHandler(2_000L);
public static ReloadLodsConfigEventHandler DELAYED_INSTANCE = new ReloadLodsConfigEventHandler(AbstractDelayedConfigEventHandler.DEFAULT_TIMEOUT_IN_MS);
/** should be used for debug options so their change can be seen instantly */
public static ReloadLodsConfigEventHandler INSTANT_INSTANCE = new ReloadLodsConfigEventHandler(0);
/** how long to wait in milliseconds before applying the config changes */
private final long timeoutInMs;
private Timer cacheClearingTimer;
//=============//
// constructor //
//=============//
//region
public ReloadLodsConfigEventHandler(long timeoutInMs)
{
this.timeoutInMs = timeoutInMs;
}
public ReloadLodsConfigEventHandler(long timeoutInMs) { super(timeoutInMs); }
//endregion
//========//
// events //
//========//
//region
@Override
public void onConfigValueSet()
{
if (this.timeoutInMs > 0)
{
this.refreshRenderDataAfterTimeout();
}
else
{
clearRenderDataCache();
}
}
/** Calling this method multiple times will reset the timer */
private synchronized void refreshRenderDataAfterTimeout() // synchronized to prevent potential threading issues when adding/removing the timer
{
// stop the previous timer if one exists
if (this.cacheClearingTimer != null)
{
this.cacheClearingTimer.cancel();
}
// create a new timer task
TimerTask timerTask = new TimerTask()
{
public void run()
{
clearRenderDataCache();
}
};
this.cacheClearingTimer = TimerUtil.CreateTimer("RenderCacheClearConfigTimer");
this.cacheClearingTimer.schedule(timerTask, this.timeoutInMs);
}
private static void clearRenderDataCache()
public void onConfigTimeout()
{
IDhApiRenderProxy renderProxy = DhApi.Delayed.renderProxy;
if (renderProxy != null)
@@ -101,5 +65,8 @@ public class ReloadLodsConfigEventHandler implements IConfigListener
}
}
//endregion
}
@@ -32,7 +32,9 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.coreapi.util.StringUtil;
public class RenderBlockCacheCsvHandler implements IConfigListener
import java.util.Timer;
public class RenderBlockCacheCsvHandler extends AbstractDelayedConfigEventHandler
{
private static final DhLogger LOGGER = new DhLoggerBuilder().build();
@@ -43,18 +45,22 @@ public class RenderBlockCacheCsvHandler implements IConfigListener
//=============//
// constructor //
//=============//
//region
/** private since we only ever need one handler at a time */
private RenderBlockCacheCsvHandler() { }
private RenderBlockCacheCsvHandler() { super(AbstractDelayedConfigEventHandler.DEFAULT_TIMEOUT_IN_MS); }
//endregion
//=================//
// config handling //
//=================//
//region
@Override
public void onConfigValueSet()
public void onConfigTimeout()
{
IWrapperFactory wrapperFactory = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
if (wrapperFactory != null)
@@ -64,6 +70,8 @@ public class RenderBlockCacheCsvHandler implements IConfigListener
}
}
//endregion
}
@@ -55,6 +55,16 @@ public class ConfigEntry<T> extends AbstractConfigBase<T>
@Nullable
private T apiValue;
/**
* Will be null if un-set. <br> <br>
*
* Some options aren't supported on all Minecraft versions,
* in those cases this value will be set to override the
* config file option.
*/
@Nullable
private T mcVersionOverrideValue;
//=============//
@@ -127,7 +137,14 @@ public class ConfigEntry<T> extends AbstractConfigBase<T>
return this.allowApiOverride
&& this.apiValue != null;
}
/** setting to null will allow the config to be used normally */
public void setMcVersionOverrideValue(@Nullable T value)
{ this.mcVersionOverrideValue = value; }
public boolean mcVersionOverridePresent()
{ return this.mcVersionOverrideValue != null; }
/**
* Should only be used when loading the config from file. <Br>
* Sets the value without informing the rest of the code (ie, it doesn't call listeners, or saving the value to file).
@@ -183,6 +200,12 @@ public class ConfigEntry<T> extends AbstractConfigBase<T>
@Override
public T get()
{
// always use the MC version specific option if defined
if (this.mcVersionOverrideValue != null)
{
return this.mcVersionOverrideValue;
}
if (this.allowApiOverride
&& this.apiValue != null)
{
@@ -59,9 +59,19 @@ public final class BufferQuad
public boolean hasError = false;
// Pre-computed sort keys to avoid recomputing on every comparison
// Slight increase in memory for reduction in cpu usage
public final long sortKeyEastWest;
public final long sortKeyNorthSouth;
BufferQuad(
//=============//
// constructor //
//=============//
//region
public BufferQuad(
short x, short y, short z, short widthEastWest, short widthNorthSouthOrHeight,
int color, byte irisBlockMaterialId, byte skylight, byte blockLight,
EDhDirection direction)
@@ -85,64 +95,46 @@ public final class BufferQuad
this.skyLight = skylight;
this.blockLight = blockLight;
this.direction = direction;
this.sortKeyEastWest = computeSortKey(direction, true);
this.sortKeyNorthSouth = computeSortKey(direction, false);
}
/** a rough but fast calculation */
double calculateDistance(double relativeX, double relativeY, double relativeZ)
private long computeSortKey(EDhDirection dir, boolean eastWest)
{
return Math.pow(relativeX - this.x, 2) + Math.pow(relativeY - this.y, 2) + Math.pow(relativeZ - this.z, 2);
if (eastWest)
{
switch (dir.axis)
{
case X: return (long) x << 48 | (long) y << 32 | (long) z << 16;
case Y: return (long) y << 48 | (long) z << 32 | (long) x << 16;
case Z: return (long) z << 48 | (long) y << 32 | (long) x << 16;
default: throw new IllegalArgumentException("Invalid Axis enum: [" + dir.axis + "].");
}
}
else
{
switch (dir.axis)
{
case X: return (long) x << 48 | (long) z << 32 | (long) y << 16;
case Y: return (long) y << 48 | (long) x << 32 | (long) z << 16;
case Z: return (long) z << 48 | (long) x << 32 | (long) y << 16;
default: throw new IllegalArgumentException("Invalid Axis enum: [" + dir.axis + "].");
}
}
}
/** compares this quad's position to the given quad */
//endregion
/** compares this quad's position to the given quad using pre-computed sort keys */
public int compare(BufferQuad quad, BufferMergeDirectionEnum compareDirection)
{
if (this.direction != quad.direction)
throw new IllegalArgumentException("The other quad is not in the same direction: " + quad.direction + " vs " + this.direction);
if (compareDirection == BufferMergeDirectionEnum.EastWest)
{
switch (this.direction.axis)
{
case X:
return threeDimensionalCompare(this.x, this.y, this.z, quad.x, quad.y, quad.z);
case Y:
return threeDimensionalCompare(this.y, this.z, this.x, quad.y, quad.z, quad.x);
case Z:
return threeDimensionalCompare(this.z, this.y, this.x, quad.z, quad.y, quad.x);
default:
throw new IllegalArgumentException("Invalid Axis enum: [" + this.direction.axis + "].");
}
}
else
{
switch (this.direction.axis)
{
case X:
return threeDimensionalCompare(this.x, this.z, this.y, quad.x, quad.z, quad.y);
case Y:
return threeDimensionalCompare(this.y, this.x, this.z, quad.y, quad.x, quad.z);
case Z:
return threeDimensionalCompare(this.z, this.x, this.y, quad.z, quad.x, quad.y);
default:
throw new IllegalArgumentException("Invalid Axis enum: [" + this.direction.axis + "].");
}
}
}
/**
* Compares two 3D points A and B. <br>
* The X, Y, and Z coordinates can be passed into parameters 0, 1, and 2 in any order
* provided they are in the same order for both A and B. <br>
* With the 0th parameter being the most significant when comparing.
*/
private static int threeDimensionalCompare(short a0, short a1, short a2, short b0, short b1, short b2)
{
long a = (long) a0 << 48 | (long) a1 << 32 | (long) a2 << 16;
long b = (long) b0 << 48 | (long) b1 << 32 | (long) b2 << 16;
return Long.compare(a, b);
return compareDirection == BufferMergeDirectionEnum.EastWest
? Long.compare(this.sortKeyEastWest, quad.sortKeyEastWest)
: Long.compare(this.sortKeyNorthSouth, quad.sortKeyNorthSouth);
}
@@ -154,11 +146,15 @@ public final class BufferQuad
public boolean tryMerge(BufferQuad quad, BufferMergeDirectionEnum mergeDirection)
{
if (quad.hasError || this.hasError)
{
return false;
}
// only merge quads that are in the same direction
if (this.direction != quad.direction)
{
return false;
}
// make sure these quads share the same perpendicular axis
if ((mergeDirection == BufferMergeDirectionEnum.EastWest && this.y != quad.y)
@@ -175,7 +171,6 @@ public final class BufferQuad
short otherParallelCompareStartPos;
switch (this.direction.axis)
{
default: // shouldn't normally happen, just here to make the compiler happy
case X:
if (mergeDirection == BufferMergeDirectionEnum.EastWest)
{
@@ -232,6 +227,9 @@ public final class BufferQuad
otherParallelCompareStartPos = quad.z;
}
break;
default: // shouldn't normally happen, just here to make the compiler happy
throw new IllegalArgumentException("Unsupported axis: ["+this.direction.axis+"]");
}
// get the width of this quad in the relevant axis
@@ -333,4 +331,6 @@ public final class BufferQuad
return true;
}
}
@@ -24,6 +24,7 @@ import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.util.objects.pooling.PhantomArrayListCheckout;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.coreapi.util.ColorUtil;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.RenderDataPointUtil;
@@ -60,6 +61,12 @@ public class ColumnBox
// variable setup //
//================//
IClientLevelWrapper clientLevelWrapper = clientLevel.getClientLevelWrapper();
if (clientLevelWrapper == null)
{
LodUtil.assertNotReach("addBoxQuadsToBuilder getClientLevelWrapper should always succeed");
}
short maxX = (short) (minX + width);
short maxY = (short) (minY + yHeight);
short maxZ = (short) (minZ + width);
@@ -122,7 +129,7 @@ public class ColumnBox
&& !isTopTransparent;
if (!skipTop)
{
builder.addQuadUp(minX, maxY, minZ, width, ColorUtil.applyShade(color, MC_RENDER.getShade(EDhDirection.UP)), irisBlockMaterialId, skyLightTop, blockLight);
builder.addQuadUp(minX, maxY, minZ, width, ColorUtil.applyShade(color, clientLevelWrapper.getShade(EDhDirection.UP)), irisBlockMaterialId, skyLightTop, blockLight);
}
}
@@ -133,7 +140,7 @@ public class ColumnBox
&& !isBottomTransparent;
if (!skipBottom)
{
builder.addQuadDown(minX, minY, minZ, width, ColorUtil.applyShade(color, MC_RENDER.getShade(EDhDirection.DOWN)), irisBlockMaterialId, skyLightBot, blockLight);
builder.addQuadDown(minX, minY, minZ, width, ColorUtil.applyShade(color, clientLevelWrapper.getShade(EDhDirection.DOWN)), irisBlockMaterialId, skyLightBot, blockLight);
}
}
@@ -163,7 +170,7 @@ public class ColumnBox
else
{
makeAdjVerticalQuad(
builder, phantomArrayCheckout,
builder, phantomArrayCheckout, clientLevelWrapper,
adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.NORTH,
minX, minY, minZ, width, yHeight,
color, irisBlockMaterialId, blockLight);
@@ -188,7 +195,7 @@ public class ColumnBox
else
{
makeAdjVerticalQuad(
builder, phantomArrayCheckout,
builder, phantomArrayCheckout, clientLevelWrapper,
adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.SOUTH,
minX, minY, maxZ, width, yHeight,
color, irisBlockMaterialId, blockLight);
@@ -213,7 +220,7 @@ public class ColumnBox
else
{
makeAdjVerticalQuad(
builder, phantomArrayCheckout,
builder, phantomArrayCheckout, clientLevelWrapper,
adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.WEST,
minX, minY, minZ, width, yHeight,
color, irisBlockMaterialId, blockLight);
@@ -238,7 +245,7 @@ public class ColumnBox
else
{
makeAdjVerticalQuad(
builder, phantomArrayCheckout,
builder, phantomArrayCheckout, clientLevelWrapper,
adjCol, adjSameDetailLevel, caveCullingMaxY, EDhDirection.EAST,
maxX, minY, minZ, width, yHeight,
color, irisBlockMaterialId, blockLight);
@@ -247,7 +254,7 @@ public class ColumnBox
}
private static void makeAdjVerticalQuad(
LodQuadBuilder builder, PhantomArrayListCheckout phantomArrayCheckout,
LodQuadBuilder builder, PhantomArrayListCheckout phantomArrayCheckout, IClientLevelWrapper clientLevelWrapper,
@NotNull ColumnRenderView adjColumnView, boolean adjacentIsSameDetailLevel, int caveCullingMaxY, EDhDirection direction,
short x, short yMin, short z, short horizontalWidth, short ySize,
int color, byte irisBlockMaterialId, byte blockLight)
@@ -263,7 +270,7 @@ public class ColumnBox
// no adjacent data //
//==================//
color = ColorUtil.applyShade(color, MC_RENDER.getShade(direction));
color = ColorUtil.applyShade(color, clientLevelWrapper.getShade(direction));
if (adjColumnView.size == 0
|| RenderDataPointUtil.hasZeroHeight(adjColumnView.get(0)))
@@ -341,13 +348,24 @@ public class ColumnBox
// Apply light to the range [adjMinY, adjMaxY)
applyLightToRange(segments, newSegments, adjMinY, adjMaxY, lightToApply);
applyLightToRangeAndPopulateNewSgements(segments, newSegments, adjMinY, adjMaxY, lightToApply);
{
// swap references so we can use the newly populated segments
LongArrayList temp = segments;
segments = newSegments;
newSegments = temp;
}
// Fill overhang area [adjMaxY, adjAboveMinY) with adjSkyLight
short adjAboveMinY = RenderDataPointUtil.getYMin(adjAbovePoint);
if (adjMaxY < adjAboveMinY)
{
applyLightToRange(segments, newSegments, adjMaxY, adjAboveMinY, adjSkyLight);
applyLightToRangeAndPopulateNewSgements(segments, newSegments, adjMaxY, adjAboveMinY, adjSkyLight);
{
LongArrayList temp = segments;
segments = newSegments;
newSegments = temp;
}
}
}
@@ -373,10 +391,11 @@ public class ColumnBox
/**
* Apply the new light value over the given y range,
* splitting segments as needed
* and putting the new segments into "newSegments"
* <p>
* source: claude.ai
*/
private static void applyLightToRange(
private static void applyLightToRangeAndPopulateNewSgements(
LongArrayList segments, LongArrayList newSegments,
short rangeStart, short rangeEnd,
byte newLight)
@@ -419,9 +438,6 @@ public class ColumnBox
newSegments.add(YSegmentUtil.encode(rangeEnd, endY, skyLight));
}
}
segments.clear();
segments.addAll(newSegments);
}
private static void tryAddVerticalFaceWithSkyLightToBuilder(
@@ -55,26 +55,6 @@ public class ColumnRenderBufferBuilder
// vbo building //
//==============//
/** @link adjData should be null for adjacent sections that cross detail level boundaries */
public static CompletableFuture<LodBufferContainer> uploadBuffersAsync(
IDhClientLevel clientLevel,
long pos,
LodQuadBuilder quadBuilder
)
{
DhBlockPos minBlockPos = new DhBlockPos(DhSectionPos.getMinCornerBlockX(pos), clientLevel.getLevelWrapper().getMinHeight(), DhSectionPos.getMinCornerBlockZ(pos));
LodBufferContainer bufferContainer = new LodBufferContainer(pos, minBlockPos);
CompletableFuture<LodBufferContainer> uploadFuture = bufferContainer.tryMakeAndUploadBuffersAsync(quadBuilder);
uploadFuture.whenComplete((uploadedBuffer, exception) ->
{
// clean up if not uploaded
if (uploadedBuffer != null && !uploadedBuffer.buffersUploaded)
{
uploadedBuffer.close();
}
});
return uploadFuture;
}
public static void makeLodRenderData(
LodQuadBuilder quadBuilder, ColumnRenderSource renderSource, IDhClientLevel clientLevel,
ColumnRenderSource[] adjRegions, boolean[] isSameDetailLevel)
@@ -20,13 +20,13 @@
package com.seibel.distanthorizons.core.dataObjects.render.bufferBuilding;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.render.RenderThreadTaskHandler;
import com.seibel.distanthorizons.core.util.ExceptionUtil;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.AbstractDhRenderApiDefinition;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.objects.ILodContainerUniformBufferWrapper;
@@ -37,7 +37,6 @@ import org.lwjgl.system.MemoryUtil;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
/**
* Java representation of one or more OpenGL buffers for rendering.
@@ -63,8 +62,6 @@ public class LodBufferContainer implements AutoCloseable
public ILodContainerUniformBufferWrapper uniformContainer = WRAPPER_FACTORY.createLodContainerUniformWrapper();
private final AtomicReference<CompletableFuture<LodBufferContainer>> uploadFutureRef = new AtomicReference<>(null);
//==============//
@@ -72,7 +69,7 @@ public class LodBufferContainer implements AutoCloseable
//==============//
//region
public LodBufferContainer(long pos, DhBlockPos minCornerBlockPos)
private LodBufferContainer(long pos, DhBlockPos minCornerBlockPos)
{
this.pos = pos;
this.minCornerBlockPos = minCornerBlockPos;
@@ -92,41 +89,12 @@ public class LodBufferContainer implements AutoCloseable
//region
/** Should be run on a DH thread. */
public synchronized CompletableFuture<LodBufferContainer> tryMakeAndUploadBuffersAsync(LodQuadBuilder builder)
public static CompletableFuture<LodBufferContainer> tryMakeAndUploadBuffersAsync(
long pos, IDhClientLevel clientLevel,
LodQuadBuilder builder)
{
//================//
// handle futures //
//================//
//region
// separate variable to prevent race condition when checking null
CompletableFuture<LodBufferContainer> oldFuture = this.uploadFutureRef.get();
if (oldFuture != null)
{
// upload already in process
return oldFuture;
}
// new upload needed
CompletableFuture<LodBufferContainer> future = new CompletableFuture<>();
future.handle((lodBufferContainer, throwable) ->
{
if (!this.uploadFutureRef.compareAndSet(future, null))
{
LOGGER.warn("upload future ref changed for pos ["+DhSectionPos.toString(this.pos)+"].");
}
return null;
});
if (!this.uploadFutureRef.compareAndSet(null, future))
{
oldFuture = this.uploadFutureRef.get();
LodUtil.assertTrue(oldFuture != null, "Concurrency error");
return oldFuture;
}
//endregion
@@ -135,91 +103,119 @@ public class LodBufferContainer implements AutoCloseable
//================//
//region
DhBlockPos minCornerBlockPos = new DhBlockPos(
DhSectionPos.getMinCornerBlockX(pos),
clientLevel.getLevelWrapper().getMinHeight(),
DhSectionPos.getMinCornerBlockZ(pos));
LodBufferContainer bufferContainer = new LodBufferContainer(pos, minCornerBlockPos);
// create CPU vertex buffers
ArrayList<ByteBuffer> opaqueBuffers = builder.makeOpaqueVertexBuffers();
ArrayList<ByteBuffer> transparentBuffers = builder.makeTransparentVertexBuffers();
this.vboOpaqueWrappers = resizeWrapperArray(this.vboOpaqueWrappers, opaqueBuffers.size());
this.vboTransparentWrappers = resizeWrapperArray(this.vboTransparentWrappers, transparentBuffers.size());
// update arrays to contain buffers
bufferContainer.vboOpaqueWrappers = resizeWrapperArray(bufferContainer.vboOpaqueWrappers, opaqueBuffers.size());
bufferContainer.vboTransparentWrappers = resizeWrapperArray(bufferContainer.vboTransparentWrappers, transparentBuffers.size());
// mac requires separate IBO objects for each VBO when using OpenGL,
// create CPU index buffers if needed.
// Mac requires separate IBO objects for each VBO when using OpenGL,
// all other OS's can share a single IBO for quicker loading times
boolean useSingleIbo = RENDER_DEF.useSingleIbo();
@Nullable ArrayList<ByteBuffer> opaqueIndexBuffers = useSingleIbo ? null : this.createIndexBuffers(opaqueBuffers);
@Nullable ArrayList<ByteBuffer> transparentIndexBuffers = useSingleIbo ? null : this.createIndexBuffers(transparentBuffers);
@Nullable ArrayList<ByteBuffer> opaqueIndexBuffers = useSingleIbo ? null : bufferContainer.createIndexBuffers(opaqueBuffers);
@Nullable ArrayList<ByteBuffer> transparentIndexBuffers = useSingleIbo ? null : bufferContainer.createIndexBuffers(transparentBuffers);
//endregion
//================//
// upload buffers //
//================//
//region
//=============//
// create VBOs //
//=============//
//region
try
CompletableFuture<Void> createFuture = new CompletableFuture<Void>();
RenderThreadTaskHandler.INSTANCE.queueRunningOnRenderThread("LodBufferContainer Setup", () ->
{
//=============//
// create VBOs //
//=============//
CompletableFuture<Void> createOpaqueFuture = createBufferWrappersAsync(future, this.vboOpaqueWrappers, opaqueBuffers);
CompletableFuture<Void> createTransparentFuture = createBufferWrappersAsync(future, this.vboTransparentWrappers, transparentBuffers);
CompletableFuture<Void> createFuture = CompletableFuture.allOf(createOpaqueFuture, createTransparentFuture);
createFuture.exceptionally((Throwable e) ->
try
{
// create VBOs failed //
// skip this event if requested
if (Thread.interrupted()
|| future.isCancelled())
{
throw new InterruptedException();
}
createBufferWrappers(bufferContainer.vboOpaqueWrappers, opaqueBuffers);
createBufferWrappers(bufferContainer.vboTransparentWrappers, transparentBuffers);
createFuture.complete(null);
}
catch (Exception e)
{
if (!ExceptionUtil.isShutdownException(e))
{
LOGGER.error("Unexpected issue creating buffer [" + this.minCornerBlockPos + "], error: [" + e.getMessage() + "].", e);
LOGGER.error("Unexpected issue creating buffers for pos: ["+DhSectionPos.toString(bufferContainer.pos)+"], error: ["+e.getMessage()+"].", e);
}
bufferContainer.close();
createFuture.completeExceptionally(e);
}
});
//endregion
//====================//
// upload VBOs to GPU //
//====================//
//region
createFuture.exceptionally((Throwable e) ->
{
// create VBOs failed //
if (!ExceptionUtil.isShutdownException(e))
{
LOGGER.error("Unexpected issue creating buffer [" + bufferContainer.minCornerBlockPos + "], error: [" + e.getMessage() + "].", e);
}
bufferContainer.close();
future.completeExceptionally(e);
return null;
});
createFuture.thenRun(() ->
{
CompletableFuture<Void> opaqueFuture = uploadBuffersAsync(future, bufferContainer.vboOpaqueWrappers, opaqueBuffers, opaqueIndexBuffers);
CompletableFuture<Void> transparentFuture = uploadBuffersAsync(future, bufferContainer.vboTransparentWrappers, transparentBuffers, transparentIndexBuffers);
CompletableFuture<Void> uploadFuture = CompletableFuture.allOf(opaqueFuture, transparentFuture);
uploadFuture.exceptionally((Throwable e) ->
{
// upload failed //
if (!ExceptionUtil.isShutdownException(e))
{
LOGGER.error("Unexpected issue uploading buffer [" + bufferContainer.minCornerBlockPos + "], error: [" + e.getMessage() + "].", e);
}
bufferContainer.close();
future.completeExceptionally(e);
return null;
});
createFuture.thenRun(() ->
uploadFuture.thenRun(() ->
{
//=============//
// upload VBOs //
//=============//
CompletableFuture<Void> opaqueFuture = uploadBuffersAsync(future, this.vboOpaqueWrappers, opaqueBuffers, opaqueIndexBuffers);
CompletableFuture<Void> transparentFuture = uploadBuffersAsync(future, this.vboTransparentWrappers, transparentBuffers, transparentIndexBuffers);
CompletableFuture<Void> uploadFuture = CompletableFuture.allOf(opaqueFuture, transparentFuture);
uploadFuture.exceptionally((Throwable e) ->
{
// upload failed //
if (!ExceptionUtil.isShutdownException(e))
{
LOGGER.error("Unexpected issue uploading buffer [" + this.minCornerBlockPos + "], error: [" + e.getMessage() + "].", e);
}
future.completeExceptionally(e);
return null;
});
uploadFuture.thenRun(() ->
{
// upload success /
this.buffersUploaded = true;
future.complete(this);
});
// upload success //
bufferContainer.buffersUploaded = true;
future.complete(bufferContainer);
});
}
catch (Exception e)
{
if (!ExceptionUtil.isShutdownException(e))
{
LOGGER.error("Unexpected issue prepping buffer uploading [" + this.minCornerBlockPos + "], error: [" + e.getMessage() + "].", e);
}
future.completeExceptionally(e);
}
});
//endregion
//================//
// buffer cleanup //
//================//
//====================//
// CPU Buffer cleanup //
//====================//
//region
future.whenComplete((LodBufferContainer lodBufferContainer, Throwable throwable) ->
{
@@ -290,11 +286,8 @@ public class LodBufferContainer implements AutoCloseable
return newVbos;
}
private static CompletableFuture<Void> createBufferWrappersAsync(
CompletableFuture<LodBufferContainer> parentFuture,
IVertexBufferWrapper[] vboWrappers, ArrayList<ByteBuffer> vertexBuffers)
private static void createBufferWrappers(IVertexBufferWrapper[] vboWrappers, ArrayList<ByteBuffer> vertexBuffers)
{
ArrayList<CompletableFuture<Void>> createVboFutureList = new ArrayList<>();
for (int i = 0; i < vertexBuffers.size(); i++)
{
if (i >= vboWrappers.length)
@@ -304,45 +297,9 @@ public class LodBufferContainer implements AutoCloseable
if (vboWrappers[i] == null)
{
final int finalVboIndex = i;
CompletableFuture<Void> future = new CompletableFuture<>();
createVboFutureList.add(future);
RenderThreadTaskHandler.INSTANCE.queueRunningOnRenderThread("LodBufferContainer Setup", () ->
{
try
{
// skip this event if requested
if (Thread.interrupted()
|| parentFuture.isCancelled())
{
throw new InterruptedException();
}
vboWrappers[finalVboIndex] = WRAPPER_FACTORY.createVboWrapper("distantHorizons:McLodRenderer");
future.complete(null);
}
catch (Exception e)
{
future.completeExceptionally(e);
}
});
vboWrappers[i] = WRAPPER_FACTORY.createVboWrapper("distantHorizons:McLodRenderer");
}
}
if (createVboFutureList.size() == 0)
{
return CompletableFuture.completedFuture(null);
}
CompletableFuture<?>[] futureArray = new CompletableFuture[createVboFutureList.size()];
for (int i = 0; i < createVboFutureList.size(); i++)
{
futureArray[i] = createVboFutureList.get(i);
}
return CompletableFuture.allOf(futureArray);
}
/** Index buffers should be null if {@link AbstractDhRenderApiDefinition#useSingleIbo()} returns true. */
@@ -365,8 +322,6 @@ public class LodBufferContainer implements AutoCloseable
// final variables for use in lambdas //
final int finalVboIndex = vboIndex;
final IVertexBufferWrapper finalVboWrapper = vboWrappers[vboIndex];
final ByteBuffer finalVertexBuffer = vertexBuffers.get(vboIndex);
@@ -385,6 +340,8 @@ public class LodBufferContainer implements AutoCloseable
CompletableFuture<Void> vertexUploadFuture = new CompletableFuture<>();
uploadFutureList.add(vertexUploadFuture);
final StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
RenderThreadTaskHandler.INSTANCE.queueRunningOnRenderThread("LodBufferContainer VBO Upload", () ->
{
try
@@ -396,21 +353,12 @@ public class LodBufferContainer implements AutoCloseable
throw new InterruptedException();
}
try
{
finalVboWrapper.uploadVertexBuffer(finalVertexBuffer, finalVertexCount);
vertexUploadFuture.complete(null);
}
catch (Exception e)
{
vboWrappers[finalVboIndex] = null;
finalVboWrapper.close();
LOGGER.error("Failed to upload buffer. Error: [" + e.getMessage() + "].", e);
}
finalVboWrapper.uploadVertexBuffer(finalVertexBuffer, finalVertexCount);
vertexUploadFuture.complete(null);
}
catch (Exception e)
{
LOGGER.error("Failed to upload buffer. Error: [" + e.getMessage() + "].", e);
vertexUploadFuture.completeExceptionally(e);
}
});
@@ -445,6 +393,7 @@ public class LodBufferContainer implements AutoCloseable
}
catch (Exception e)
{
finalVboWrapper.close();
indexUploadFuture.completeExceptionally(e);
}
});
@@ -532,26 +481,29 @@ public class LodBufferContainer implements AutoCloseable
RenderThreadTaskHandler.INSTANCE.queueRunningOnRenderThread("LodBufferContainer Close", () ->
{
for (IVertexBufferWrapper buffer : this.vboOpaqueWrappers)
{
if (buffer != null)
{
buffer.close();
}
}
for (IVertexBufferWrapper buffer : this.vboTransparentWrappers)
{
if (buffer != null)
{
buffer.close();
}
}
tryCloseBufferWrapperArray(this.vboOpaqueWrappers);
tryCloseBufferWrapperArray(this.vboTransparentWrappers);
this.uniformContainer.close();
});
}
private static void tryCloseBufferWrapperArray(@Nullable IVertexBufferWrapper[] bufferWrappers)
{
if (bufferWrappers != null)
{
for (int i = 0; i < bufferWrappers.length; i++)
{
IVertexBufferWrapper buffer = bufferWrappers[i];
bufferWrappers[i] = null;
if (buffer != null)
{
buffer.close();
}
}
}
}
//endregion
@@ -61,7 +61,7 @@ public class LodQuadBuilder
public static final int BYTES_PER_QUAD = BYTES_PER_VERTEX * 4;
public static final int[][][] DIRECTION_VERTEX_IBO_QUAD = new int[][][]
///region
//region
{
// X,Z //
{ // UP
@@ -109,7 +109,7 @@ public class LodQuadBuilder
{0, 0}, // 3
},
};
///endregion
//endregion
private int premergeCount = 0;
@@ -394,7 +394,7 @@ public class LodQuadBuilder
// for horizontal and bottom faces of grass blocks, use the dirt color to
// prevent green cliff walls
color = this.clientLevelWrapper.getDirtBlockColor();
color = ColorUtil.applyShade(color, MC_RENDER.getShade(quad.direction));
color = ColorUtil.applyShade(color, this.clientLevelWrapper.getShade(quad.direction));
}
}
}
@@ -129,6 +129,7 @@ public class FullDataToRenderDataTransformer
ColumnRenderView tempExpandingColumnView = ColumnRenderView.getPooled();
RenderDataPointReducingList reducingList = new RenderDataPointReducingList())
{
DhBlockPosMutable mutableBlockPos = new DhBlockPosMutable();
for (int x = 0; x < FullDataSourceV2.WIDTH; x++)
{
for (int z = 0; z < FullDataSourceV2.WIDTH; z++)
@@ -142,7 +143,7 @@ public class FullDataToRenderDataTransformer
baseX + BitShiftUtil.pow(x, dataDetail), baseZ + BitShiftUtil.pow(z, dataDetail),
columnArrayView, dataColumn,
// pooled references so we don't need to re-allocate/get them 4000 times per render source
phantomCheckout, tempExpandingColumnView, reducingList);
phantomCheckout, tempExpandingColumnView, reducingList, mutableBlockPos);
}
}
}
@@ -157,7 +158,7 @@ public class FullDataToRenderDataTransformer
ColumnRenderView columnArrayView,
LongArrayList fullDataColumn,
// pooled references
PhantomArrayListCheckout phantomCheckout, ColumnRenderView tempExpandingColumnView, RenderDataPointReducingList reducingList)
PhantomArrayListCheckout phantomCheckout, ColumnRenderView tempExpandingColumnView, RenderDataPointReducingList reducingList, DhBlockPosMutable mutableBlockPos)
{
// we can't do anything if the full data is missing or empty
if (fullDataColumn == null
@@ -170,7 +171,7 @@ public class FullDataToRenderDataTransformer
if (fullDataLength <= columnArrayView.maxVerticalSliceCount)
{
// Directly use the arrayView since it fits.
setRenderColumnView(levelWrapper, fullDataSource, blockX, blockZ, columnArrayView, fullDataColumn);
setRenderColumnView(levelWrapper, fullDataSource, blockX, blockZ, columnArrayView, fullDataColumn, mutableBlockPos);
}
else
{
@@ -178,7 +179,7 @@ public class FullDataToRenderDataTransformer
// expand the ColumnArrayView to fit the new larger max vertical size
tempExpandingColumnView.populate(dataArrayList, fullDataLength, 0, fullDataLength);
setRenderColumnView(levelWrapper, fullDataSource, blockX, blockZ, tempExpandingColumnView, fullDataColumn);
setRenderColumnView(levelWrapper, fullDataSource, blockX, blockZ, tempExpandingColumnView, fullDataColumn, mutableBlockPos);
columnArrayView.changeVerticalSizeFrom(tempExpandingColumnView, reducingList);
}
@@ -186,7 +187,7 @@ public class FullDataToRenderDataTransformer
private static void setRenderColumnView(
IClientLevelWrapper levelWrapper, FullDataSourceV2 fullDataSource,
int blockX, int blockZ,
ColumnRenderView renderColumnData, LongArrayList fullColumnData)
ColumnRenderView renderColumnData, LongArrayList fullColumnData, DhBlockPosMutable mutableBlockPos)
{
//===============//
// config values //
@@ -242,7 +243,8 @@ public class FullDataToRenderDataTransformer
FullDataPointIdMap fullDataMapping = fullDataSource.mapping;
DhBlockPosMutable mutableBlockPos = new DhBlockPosMutable(blockX, 0, blockZ);
mutableBlockPos.setX(blockX);
mutableBlockPos.setZ(blockZ);
// goes from the top down
for (int fullDataIndex = 0; fullDataIndex < fullColumnData.size(); fullDataIndex++)
@@ -450,8 +452,17 @@ public class FullDataToRenderDataTransformer
// use the previous block's color
color = colorToApplyToNextBlock;
colorToApplyToNextBlock = -1;
skyLight = skylightToApplyToNextBlock;
blockLight = blocklightToApplyToNextBlock;
// use the skylight override if present
if (skylightToApplyToNextBlock != -1)
{
skyLight = skylightToApplyToNextBlock;
}
if (blocklightToApplyToNextBlock != -1)
{
blockLight = blocklightToApplyToNextBlock;
}
}
@@ -91,10 +91,27 @@ public class LodRequestModule implements Closeable
{
try
{
// Initial wait is to prevent an issue
// where this starts before the child object's constructor finishes,
// causing null pointers on final non-null references.
// The try-catch in the while loop should also handle this
// but this way we shouldn't have error logs.
Thread.sleep(500);
// run until the threadpool is shut down
while (!Thread.interrupted())
{
Thread.sleep(20);
this.tick();
try
{
Thread.sleep(20);
this.tick();
}
catch (InterruptedException e) { throw e; }
catch (Exception e)
{
LOGGER.error("Unexpected error in [" + LodRequestModule.class.getSimpleName() + "] tick loop, error: [" + e.getMessage() + "].", e);
}
}
}
catch (InterruptedException ignore) { }
@@ -104,18 +121,7 @@ public class LodRequestModule implements Closeable
boolean shouldDoWorldGen = this.onWorldGenCompleteListener.shouldDoWorldGen();
// if the world is read only don't generate anything
shouldDoWorldGen &= !DhApiWorldProxy.INSTANCE.getReadOnly();
// don't generate chunks for client levels that aren't being rendered
// (this can happen when moving between dimensions)
if (this.level instanceof IDhClientLevel)
{
boolean isRendering = ((IDhClientLevel) this.level).isRendering();
if (!isRendering)
{
shouldDoWorldGen = false;
}
}
shouldDoWorldGen &= !DhApiWorldProxy.INSTANCE.tryGetReadOnly();
@@ -202,7 +202,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
private synchronized void tryQueueNewWorldGenRequestsAsync()
{
if (!DhApiWorldProxy.INSTANCE.worldLoaded()
|| DhApiWorldProxy.INSTANCE.getReadOnly())
|| DhApiWorldProxy.INSTANCE.tryGetReadOnly())
{
return;
}
@@ -9,7 +9,6 @@ import com.seibel.distanthorizons.core.multiplayer.server.FullDataSourceRequestH
import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerState;
import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerStateManager;
import com.seibel.distanthorizons.core.network.exceptions.RequestOutOfRangeException;
import com.seibel.distanthorizons.core.network.exceptions.RequestRejectedException;
import com.seibel.distanthorizons.core.network.exceptions.SectionRequiresSplittingException;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import com.seibel.distanthorizons.core.network.messages.AbstractTrackableMessage;
@@ -200,26 +199,6 @@ public abstract class AbstractDhServerLevel extends AbstractDhLevel implements I
LodUtil.assertTrue(message.getSession().serverPlayer != null);
// Check if the player is in this dimension,
// since handling multiple dimensions isn't allowed
if (message.getSession().serverPlayer.getLevel() != this.getLevelWrapper())
{
// If the message can be replied to - reply with an error, otherwise just ignore
if (message instanceof AbstractTrackableMessage)
{
((AbstractTrackableMessage) message).sendResponse(
new RequestRejectedException(
"Generation not allowed. " +
"Requested dimension: ["+((ILevelRelatedMessage) message).getLevelName()+"], " +
"player dimension: [" + message.getSession().serverPlayer.getLevel().getDhIdentifier() + "], " +
"handler dimension: [" + this.getLevelWrapper().getDhIdentifier() + "]"
)
);
}
return false;
}
return true;
}
@@ -21,16 +21,21 @@ package com.seibel.distanthorizons.core.level;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.ModAccessorInjector;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.file.fullDatafile.IDataSourceUpdateListenerFunc;
import com.seibel.distanthorizons.core.file.fullDatafile.V2.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.render.QuadTree.LodQuadTree;
import com.seibel.distanthorizons.core.render.RenderBufferHandler;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.math.Vec3d;
import com.seibel.distanthorizons.core.wrapperInterfaces.IWrapperFactory;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor.IImmersivePortalsAccessor;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.renderPass.IDhGenericRenderer;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.core.logging.DhLogger;
@@ -44,6 +49,7 @@ public class ClientLevelModule implements Closeable, IDataSourceUpdateListenerFu
{
private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
private static final IMinecraftRenderWrapper MC_RENDER = SingletonInjector.INSTANCE.get(IMinecraftRenderWrapper.class);
private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class);
private final IDhClientLevel clientLevel;
@@ -106,7 +112,10 @@ public class ClientLevelModule implements Closeable, IDataSourceUpdateListenerFu
this.ClientRenderStateRef.set(clientRenderState);
}
clientRenderState.quadtree.tryTick(new DhBlockPos2D(MC_CLIENT.getPlayerBlockPos()));
// use camera position instead of player pos so free cam mods work better
Vec3d cameraDoublePos = MC_RENDER.getCameraExactPosition();
DhBlockPos2D cameraBlockPos = new DhBlockPos2D((int)cameraDoublePos.x, (int)cameraDoublePos.z);
clientRenderState.quadtree.tryTick(cameraBlockPos);
}
@@ -170,6 +179,7 @@ public class ClientLevelModule implements Closeable, IDataSourceUpdateListenerFu
}
this.fullDataSourceProvider.removeDataSourceUpdateListener(this);
this.genericRenderer.close();
}
@@ -259,7 +269,7 @@ public class ClientLevelModule implements Closeable, IDataSourceUpdateListenerFu
@Override
public void close()
{
LOGGER.info("Shutting down " + ClientRenderState.class.getSimpleName());
//LOGGER.info("Shutting down " + ClientRenderState.class.getSimpleName());
this.quadtree.close();
}
@@ -167,15 +167,17 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
}
// Check this before decoding data to prevent errors if multiple client levels
// are receiving data at once (Immersive Portals compatibility).
boolean isSameLevel = message.isSameLevelAs(this.levelWrapper);
//NETWORK_LOGGER.debug("Buffer ["+message.payload.dtoBufferId+"] isSameLevel: ["+isSameLevel+"]");
if (!isSameLevel)
{
return;
}
try (FullDataSourceV2DTO dataSourceDto = this.networkState.fullDataPayloadReceiver.decodeDataSource(message.payload))
{
boolean isSameLevel = message.isSameLevelAs(this.levelWrapper);
NETWORK_LOGGER.debug("Buffer ["+message.payload.dtoBufferId+"] isSameLevel: ["+isSameLevel+"]");
if (!isSameLevel)
{
return;
}
Executor executor = ThreadPoolUtil.getFileHandlerExecutor();
if (executor != null)
@@ -219,6 +221,7 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
{
try
{
if (MC_CLIENT.getWrappedClientLevel() == null || MC_CLIENT.getWrappedClientLevel().getDhLevel() != this) return;
this.clientside.clientTick();
if (this.syncOnLoadRequestQueue != null)
@@ -71,7 +71,11 @@ public class DhClientServerLevel extends AbstractDhServerLevel implements IDhCli
//region
@Override
public void clientTick() { this.clientside.clientTick(); }
public void clientTick()
{
if (MC_CLIENT.getWrappedClientLevel() == null || MC_CLIENT.getWrappedClientLevel().getDhLevel() != this) return;
this.clientside.clientTick();
}
//endregion
@@ -151,8 +151,7 @@ public abstract class AbstractFullDataNetworkRequestQueue implements IDebugRende
public synchronized boolean tick(DhBlockPos2D targetPos)
{
if (DhApiWorldProxy.INSTANCE.worldLoaded()
&& DhApiWorldProxy.INSTANCE.getReadOnly())
if (DhApiWorldProxy.INSTANCE.tryGetReadOnly())
{
return false;
}
@@ -13,6 +13,7 @@ import com.seibel.distanthorizons.core.network.event.ScopedNetworkEventSource;
import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent;
import com.seibel.distanthorizons.core.network.event.internal.IncompatibleMessageInternalEvent;
import com.seibel.distanthorizons.core.network.messages.base.LevelInitMessage;
import com.seibel.distanthorizons.core.network.messages.base.RequestLevelInitMessage;
import com.seibel.distanthorizons.core.network.messages.base.SessionConfigMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceResponseMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSplitMessage;
@@ -164,7 +165,8 @@ public class ClientNetworkState implements Closeable
// send message //
//==============//
public void sendLevelInitRequest(String clientLevelKey)
{ this.getSession().sendMessage(new RequestLevelInitMessage(clientLevelKey)); }
public void sendConfigMessage() { this.sendConfigMessage(true); }
public void sendConfigMessage(boolean blocking)
@@ -7,10 +7,10 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.network.INetworkObject;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSplitMessage;
import com.seibel.distanthorizons.core.sql.dto.FullDataSourceV2DTO;
import com.seibel.distanthorizons.core.util.LodUtil;
import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.Unpooled;
import java.util.Objects;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
@@ -21,7 +21,7 @@ public class FullDataPayloadReceiver implements AutoCloseable
.build();
private final ConcurrentMap<Integer, CompositeByteBuf> buffersById = CacheBuilder.newBuilder()
.expireAfterAccess(10, TimeUnit.SECONDS)
.expireAfterAccess(30, TimeUnit.SECONDS)
.<Integer, CompositeByteBuf>build().asMap();
@Override
@@ -56,7 +56,7 @@ public class FullDataPayloadReceiver implements AutoCloseable
public FullDataSourceV2DTO decodeDataSource(FullDataPayload payload)
{
CompositeByteBuf compositeByteBuffer = this.buffersById.get(payload.dtoBufferId);
LodUtil.assertTrue(compositeByteBuffer != null, "decoded data source missing byte buffer");
Objects.requireNonNull(compositeByteBuffer, "Unable to get a complete buffer for a received payload. Ignore this if it doesn't spam similar errors");
try
{
@@ -2,6 +2,7 @@ package com.seibel.distanthorizons.core.multiplayer.server;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.listeners.ConfigChangeListener;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.level.AbstractDhServerLevel;
import com.seibel.distanthorizons.core.multiplayer.config.SessionConfig;
import com.seibel.distanthorizons.core.multiplayer.fullData.FullDataPayloadSender;
@@ -9,20 +10,25 @@ import com.seibel.distanthorizons.core.multiplayer.fullData.SharedBandwidthLimit
import com.seibel.distanthorizons.core.network.event.internal.IncompatibleMessageInternalEvent;
import com.seibel.distanthorizons.core.network.messages.base.CloseReasonMessage;
import com.seibel.distanthorizons.core.network.messages.base.LevelInitMessage;
import com.seibel.distanthorizons.core.network.messages.base.RequestLevelInitMessage;
import com.seibel.distanthorizons.core.network.messages.base.SessionConfigMessage;
import com.seibel.distanthorizons.core.network.event.internal.CloseInternalEvent;
import com.seibel.distanthorizons.core.network.exceptions.RateLimitedException;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage;
import com.seibel.distanthorizons.core.network.session.NetworkSession;
import com.seibel.distanthorizons.core.util.ratelimiting.SupplierBasedRateAndConcurrencyLimiter;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import org.jetbrains.annotations.NotNull;
import java.io.Closeable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
public class ServerPlayerState implements Closeable
{
private final IMinecraftSharedWrapper MC_SHARED = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class);
private final ConfigChangeListener<String> levelKeyPrefixChangeListener
= new ConfigChangeListener<>(Config.Server.levelKeyPrefix, this::onLevelKeyPrefixConfigChanged);
private final SessionConfig.AnyChangeListener configAnyChangeListener = new SessionConfig.AnyChangeListener(this::sendConfigMessage);
@@ -66,6 +72,12 @@ public class ServerPlayerState implements Closeable
this.sendConfigMessage();
});
this.networkSession.registerHandler(RequestLevelInitMessage.class, (requestLevelKeyMessage) ->
{
sendLevelKey(requestLevelKeyMessage.clientLevelKey);
});
this.networkSession.registerHandler(CloseInternalEvent.class, event -> {
// No-op. prevents "Unhandled message" log entries
});
@@ -85,12 +97,27 @@ public class ServerPlayerState implements Closeable
//=================//
private void onLevelKeyPrefixConfigChanged(String newLevelKey) { this.sendLevelKey(); }
private void sendLevelKey(String clientLevelKey)
{
sendLevelKey(() ->
MC_SHARED
.getWrappedServerLevel(clientLevelKey)
.getKeyedLevelDimensionName());
}
private void sendLevelKey()
{
sendLevelKey(() ->
this.getServerPlayer()
.getLevel()
.getKeyedLevelDimensionName());
}
private void sendLevelKey(Supplier<String> levelKeySupplier)
{
if (Config.Server.sendLevelKeys.get())
{
String levelKey = levelKeySupplier.get();
// let the client's know about the change
String levelKey = this.getServerPlayer().getLevel().getKeyedLevelDimensionName();
if (!levelKey.equals(this.lastLevelKey))
{
this.lastLevelKey = levelKey;
@@ -21,12 +21,9 @@ package com.seibel.distanthorizons.core.network.messages;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.seibel.distanthorizons.core.network.messages.base.CodecCrashMessage;
import com.seibel.distanthorizons.core.network.messages.base.LevelInitMessage;
import com.seibel.distanthorizons.core.network.messages.base.SessionConfigMessage;
import com.seibel.distanthorizons.core.network.messages.base.*;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSplitMessage;
import com.seibel.distanthorizons.core.network.messages.requests.CancelMessage;
import com.seibel.distanthorizons.core.network.messages.base.CloseReasonMessage;
import com.seibel.distanthorizons.core.network.messages.requests.ExceptionMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataPartialUpdateMessage;
import com.seibel.distanthorizons.core.network.messages.fullData.FullDataSourceRequestMessage;
@@ -60,6 +57,7 @@ public class MessageRegistry
// Level keys
this.registerMessage(LevelInitMessage.class, LevelInitMessage::new);
this.registerMessage(RequestLevelInitMessage.class, RequestLevelInitMessage::new);
// Config (for full DH support)
this.registerMessage(SessionConfigMessage.class, SessionConfigMessage::new);
@@ -0,0 +1,65 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.network.messages.base;
import com.google.common.base.MoreObjects;
import com.seibel.distanthorizons.core.network.messages.AbstractNetworkMessage;
import io.netty.buffer.ByteBuf;
/** used for full DH support */
public class RequestLevelInitMessage extends AbstractNetworkMessage
{
public String clientLevelKey;
//=============//
// constructor //
//=============//
public RequestLevelInitMessage() { }
public RequestLevelInitMessage(String clientLevelKey) { this.clientLevelKey = clientLevelKey; }
//===============//
// serialization //
//===============//
@Override
public void encode(ByteBuf out) { this.writeString(this.clientLevelKey, out); }
@Override
public void decode(ByteBuf in) { this.clientLevelKey = this.readString(in); }
//================//
// base overrides //
//================//
@Override
public MoreObjects.ToStringHelper toStringHelper()
{
return super.toStringHelper()
.add("levelKey", this.clientLevelKey);
}
}
@@ -77,6 +77,8 @@ public class DhApiRenderProxy implements IDhApiRenderProxy
}
}
// TODO clear tint handler too
return DhApiResult.createSuccess();
}
@@ -21,6 +21,7 @@ package com.seibel.distanthorizons.core.render.QuadTree;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.config.listeners.IConfigListener;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.file.fullDatafile.V2.FullDataSourceProviderV2;
@@ -148,7 +149,8 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
int initialPlayerBlockX, int initialPlayerBlockZ,
FullDataSourceProviderV2 fullDataSourceProvider)
{
super(viewDiameterInBlocks, new DhBlockPos2D(initialPlayerBlockX, initialPlayerBlockZ), DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
super(viewDiameterInBlocks, FullDataSourceV2.WIDTH,
new DhBlockPos2D(initialPlayerBlockX, initialPlayerBlockZ), DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL);
DEBUG_RENDERER.register(this, Config.Client.Advanced.Debugging.DebugWireframe.showQuadTreeRenderStatus);
@@ -245,17 +247,38 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
//===================//
//region
this.setCenterBlockPos(playerPos, (renderSection) ->
{
// removing out of bounds sections
if (renderSection != null)
// remove out of bound sections
this.setCenterBlockPos(playerPos,
// remove completely out of bound nodes
// (the root node is no longer in bounds)
(renderSection) ->
{
this.fullDataSourceProvider.removeRetrievalRequestIf((long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
this.missingGenerationPosSet.remove(renderSection.pos);
this.queuedGenerationPosSet.remove(renderSection.pos);
renderSection.close();
if (renderSection != null)
{
this.fullDataSourceProvider.removeRetrievalRequestIf((long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
// unfortunately we have to fully go through each set
// since a removed position may be larger than the multiple generated positions
// it contains
this.missingGenerationPosSet.removeIf((Long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
this.queuedGenerationPosSet.removeIf((Long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
renderSection.close();
}
},
// mutate partially out of bound nodes
// (the root node is still in bounds, but this individual child node isn't)
(renderSection) ->
{
if (renderSection != null)
{
// when this node comes back into render distance
// we'll need to re-load it since the full data
// may have been modified while it was out of bounds
renderSection.renderDataDirty = true;
}
}
});
);
//endregion
@@ -304,7 +327,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
continue;
}
node.value.retreivedMissingSectionsForRetreival = false;
node.value.queuedMissingSectionsForRetrieval = false;
}
}
@@ -372,8 +395,6 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
//=========================//
//region
// also handles disabling beacons
for (QuadNode<LodRenderSection> node : this.tickNodeHolder.getDisableNodes())
{
if (node == null || node.value == null) { continue; }
@@ -449,9 +470,9 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
// since this section wants to render
// check if it needs any generation to do so
if (!node.value.retreivedMissingSectionsForRetreival)
if (!node.value.queuedMissingSectionsForRetrieval)
{
node.value.retreivedMissingSectionsForRetreival = true;
node.value.queuedMissingSectionsForRetrieval = true;
this.tryQueuePosForRetrieval(node.value.pos); // can be quite slow
}
}
@@ -476,9 +497,10 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
//=========================//
// tick - recursive update //
//=========================//
///region
//region
private void recursivelyUpdateRenderSectionNode(
/** @return true if the node at this position has uploaded its render data */
private boolean recursivelyUpdateRenderSectionNode(
@NotNull DhBlockPos2D playerPos,
@NotNull QuadNode<LodRenderSection> rootNode,
@Nullable QuadNode<LodRenderSection> parentNode,
@@ -490,19 +512,22 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
// get/create the node //
// and render section //
//=====================//
///region
//region
// create the node
if (quadNode == null)
{
rootNode.setValue(sectionPos, new LodRenderSection(sectionPos, this, this.level, this.fullDataSourceProvider));
quadNode = rootNode.getNode(sectionPos);
}
if (quadNode == null)
quadNode = this.tryAddNodeToTree(rootNode, quadNode, sectionPos);
// Skip sections that are out-of-bounds.
// If not done some sections will appear and/or generate
// outside the desired render distance
if (!this.isSectionPosInBounds(quadNode.sectionPos))
{
LodUtil.assertNotReach("Unable to add node with pos ["+DhSectionPos.toString(sectionPos)+"] to tree root ["+rootNode+"].");
this.tickNodeHolder.addDisableNode(quadNode);
this.recursivelyDisableChildNodes(quadNode);
return true;
}
// make sure the render section is created (shouldn't be necessary, but just in case)
LodRenderSection renderSection = quadNode.value;
if (renderSection == null)
@@ -511,7 +536,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
quadNode.setValue(sectionPos, renderSection);
}
///endregion
//endregion
@@ -519,7 +544,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
// handle enabling, loading, //
// and disabling render sections //
//===============================//
///region
//region
// load every node for rendering
if (!renderSection.gpuUploadInProgress()
@@ -535,94 +560,153 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
if (DhSectionPos.getDetailLevel(quadNode.sectionPos) > expectedDetailLevel)
{
this.onDetailLevelTooHigh(playerPos, rootNode, quadNode);
return this.onDetailLevelTooLow(playerPos, rootNode, quadNode);
}
// the (expectedDetailLevel-1) fixes corners being cut out due to distance calculations using the LOD center
else if (DhSectionPos.getDetailLevel(quadNode.sectionPos) == expectedDetailLevel
|| DhSectionPos.getDetailLevel(quadNode.sectionPos) == expectedDetailLevel - 1)
{
this.onDesiredDetailLevel(quadNode, parentNode);
return this.onDesiredDetailLevel(quadNode, parentNode);
}
else
{
throw new IllegalStateException("LodQuadTree shouldn't be updating renderSections below the expected detail level: [" + expectedDetailLevel + "].");
}
///endregion
//endregion
}
private void onDetailLevelTooHigh(
/** @return true if the node at this position has uploaded its render data */
private boolean onDetailLevelTooLow(
@NotNull DhBlockPos2D playerPos,
@NotNull QuadNode<LodRenderSection> rootNode, @NotNull QuadNode<LodRenderSection> quadNode)
@NotNull QuadNode<LodRenderSection> rootNode,
@NotNull QuadNode<LodRenderSection> quadNode)
{
// recursively update each child node
boolean allChildNodesCanRender = true;
int childNodeRenderCount = 0;
for (int i = 0; i < 4; i++)
{
QuadNode<LodRenderSection> childNode = quadNode.getChildByIndex(i);
long childPos = DhSectionPos.getChildByIndex(quadNode.sectionPos, i);
this.recursivelyUpdateRenderSectionNode(
playerPos,
rootNode, quadNode, childNode, childPos);
childNode = quadNode.getChildByIndex(i); // needs to be gotten again in case a new node was added to the tree (this will often happen when moving into new areas where the children were deleted)
QuadNode<LodRenderSection> childNode = quadNode.getChildByIndex(i);
// nodes shouldn't be null, but just in case
if (childNode != null
&& childNode.value != null
&& !childNode.value.gpuUploadComplete())
boolean childCanRender = this.recursivelyUpdateRenderSectionNode(
playerPos,
rootNode, quadNode, childNode, childPos);
if (childCanRender)
{
// the node is present but not uploaded yet
allChildNodesCanRender = false;
// node can be rendered
childNodeRenderCount++;
}
}
if (allChildNodesCanRender)
boolean isRootNode = (quadNode == rootNode);
if (isRootNode)
{
// all child nodes can render, this node isn't needed
// Never render the root node.
// This is done to prevent flashing when moving across root node
// boundaries.
// Otherwise, when moving, new empty nodes will be added at the edge of the tree
// which will require the root node to render to cover the "empty" area.
this.tickNodeHolder.addDisableNode(quadNode);
return false;
}
else if (childNodeRenderCount >= 4)
{
this.tickNodeHolder.addDisableNode(quadNode);
// all children can render,
// the area will be filled when rendering
return true;
}
else
{
// not all child positions are loaded yet, this one should be rendered instead
this.tickNodeHolder.addEnableNode(quadNode);
boolean nodeCanRender = quadNode.value != null
&& quadNode.value.canRender();
if (nodeCanRender)
{
// not all child positions are loaded yet, this one should be rendered instead
this.tickNodeHolder.addEnableNode(quadNode);
this.recursivelyDisableChildNodes(quadNode);
}
else
{
this.tickNodeHolder.addDisableNode(quadNode);
}
return nodeCanRender;
}
}
private void onDesiredDetailLevel(
@NotNull QuadNode<LodRenderSection> quadNode, @Nullable QuadNode<LodRenderSection> parentNode)
/** @return true if the node at this position has uploaded its render data */
private boolean onDesiredDetailLevel(
@NotNull QuadNode<LodRenderSection> quadNode,
@Nullable QuadNode<LodRenderSection> parentNode)
{
boolean allAdjNodesCanRender = true;
// if the parent node is null, that means we're at the root node
// and we should always render
if (parentNode != null)
// Skip sections that are out-of-bounds.
// If not done some sections will appear and/or generate
// outside the desired render distance
if (!this.isSectionPosInBounds(quadNode.sectionPos))
{
// check if all adjacent nodes are ready to render
// this check is done to prevent some overlapping due to the parent node
// still being active
for (int i = 0; i < 4; i++)
return true;
}
if (quadNode.value != null
&& quadNode.value.canRender())
{
if (!this.tickNodeHolder.getEnabledNodes().contains(parentNode))
{
QuadNode<LodRenderSection> adjNode = parentNode.getChildByIndex(i);
// nodes shouldn't be null, but just in case there's an issue
if (adjNode != null
&& adjNode.value != null
&& !adjNode.value.gpuUploadComplete())
{
// the node is present but not uploaded yet
allAdjNodesCanRender = false;
}
this.tickNodeHolder.addEnableDeleteChildrenNode(quadNode);
return true;
}
else
{
this.tickNodeHolder.addDisableNode(quadNode);
return false;
}
}
if (allAdjNodesCanRender
&& quadNode.value != null
&& quadNode.value.gpuUploadComplete())
else
{
this.tickNodeHolder.addEnableDeleteChildrenNode(quadNode);
this.tickNodeHolder.addDisableNode(quadNode);
return false;
}
}
///endregion
@NotNull
private QuadNode<LodRenderSection> tryAddNodeToTree(
@NotNull QuadNode<LodRenderSection> rootNode,
@Nullable QuadNode<LodRenderSection> quadNode,
long sectionPos // section pos is needed here since the quad node may be null
)
{
// create the node
if (quadNode == null)
{
rootNode.setValue(sectionPos, new LodRenderSection(sectionPos, this, this.level, this.fullDataSourceProvider));
quadNode = rootNode.getNode(sectionPos);
}
if (quadNode == null)
{
LodUtil.assertNotReach("Unable to add node with pos ["+DhSectionPos.toString(sectionPos)+"] to tree root ["+rootNode+"].");
}
return quadNode;
}
private void recursivelyDisableChildNodes(@NotNull QuadNode<LodRenderSection> quadNode)
{
for (int i = 0; i < 4; i++)
{
QuadNode<LodRenderSection> childNode = quadNode.getChildByIndex(i);
this.tickNodeHolder.removeEnableAndDisableNode(childNode);
if (childNode != null)
{
this.recursivelyDisableChildNodes(childNode);
}
}
}
//endregion
//=====================//
// tick - work queuing //
@@ -1127,7 +1211,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
{
color = Color.ORANGE;
}
else if (!renderSection.gpuUploadComplete())
else if (!renderSection.canRender())
{
// uploaded but the buffer is missing
color = Color.PINK;
@@ -1169,7 +1253,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
@Override
public void close()
{
LOGGER.info("Shutting down LodQuadTree...");
//LOGGER.info("Shutting down LodQuadTree...");
DEBUG_RENDERER.unregister(this, Config.Client.Advanced.Debugging.DebugWireframe.showQuadTreeRenderStatus);
Config.Common.WorldGenerator.enableDistantGeneration.removeListener(this);
@@ -1204,7 +1288,7 @@ public class LodQuadTree extends QuadTree<LodRenderSection> implements IDebugRen
});
LOGGER.info("Finished shutting down LodQuadTree");
//LOGGER.info("Finished shutting down LodQuadTree");
}
//endregion base methods
@@ -34,11 +34,9 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
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.dataObjects.render.bufferBuilding.LodBufferContainer;
import com.seibel.distanthorizons.core.sql.dto.BeaconBeamDTO;
import com.seibel.distanthorizons.core.sql.repo.BeaconBeamRepo;
import com.seibel.distanthorizons.core.util.ExceptionUtil;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
@@ -48,10 +46,8 @@ import org.jetbrains.annotations.Nullable;
import javax.annotation.WillNotClose;
import java.awt.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
/**
* A render section represents an area that could be rendered.
@@ -67,7 +63,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
public final long pos;
private final IDhClientLevel level;
private final IDhClientLevel clientLevel;
private final IClientLevelWrapper levelWrapper;
@WillNotClose
private final FullDataSourceProviderV2 fullDataSourceProvider;
@@ -75,8 +71,16 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
private boolean renderingEnabled = false;
private boolean beaconsRendering = false;
public boolean retreivedMissingSectionsForRetreival = false;
/**
* Used when a node goes out of render distance
* but isn't removed from the underlying quad tree structure. <br><br>
*
* In those cases we should act as if the node was removed
* for cached render data caching purposes, but not
* for re-creating missing nodes.
*/
public boolean renderDataDirty = false;
public boolean queuedMissingSectionsForRetrieval = false;
/** this reference is necessary so we can determine what VBO to render */
public LodBufferContainer renderBufferContainer;
@@ -94,13 +98,6 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
*/
private Runnable getAndBuildRenderDataRunnable = null;
/**
* Represents just uploading the {@link LodQuadBuilder} to the GPU. <br>
* Separate from {@link LodRenderSection#getAndBuildRenderDataFutureRef} because they run on
* different threads (buffer uploading is on the MC render thread) and need to be canceled separately.
*/
private final AtomicReference<CompletableFuture<LodBufferContainer>> bufferUploadFutureRef = new AtomicReference<>(null);
//=============//
@@ -111,12 +108,12 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
public LodRenderSection(
long pos,
LodQuadTree quadTree,
IDhClientLevel level, FullDataSourceProviderV2 fullDataSourceProvider)
IDhClientLevel clientLevel, FullDataSourceProviderV2 fullDataSourceProvider)
{
this.pos = pos;
this.quadTree = quadTree;
this.level = level;
this.levelWrapper = level.getClientLevelWrapper();
this.clientLevel = clientLevel;
this.levelWrapper = clientLevel.getClientLevelWrapper();
this.fullDataSourceProvider = fullDataSourceProvider;
DEBUG_RENDERER.register(this, Config.Client.Advanced.Debugging.DebugWireframe.showRenderSectionStatus);
@@ -158,6 +155,8 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
try
{
// shouldn't happen since this method is synchronized, but just in case
// make sure we only ever start one upload task
if (!this.getAndBuildRenderDataFutureRef.compareAndSet(null, future))
{
CompletableFuture<Void> oldFuture = this.getAndBuildRenderDataFutureRef.get();
@@ -170,6 +169,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
{
try
{
// build LOD data on a DH thread
LodQuadBuilder lodQuadBuilder = this.getAndBuildRenderData();
if (lodQuadBuilder == null)
{
@@ -177,7 +177,8 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
return;
}
this.uploadToGpuAsync(lodQuadBuilder)
// uploading will primarily happen on the render thread
this.uploadToGpuAsync(future, lodQuadBuilder)
.thenRun(() ->
{
// the future is passed in separately (IE not using the local var) to prevent any possible race condition null pointers
@@ -187,7 +188,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
catch (Exception e)
{
LOGGER.error("Unexpected issue creating render data for pos: ["+DhSectionPos.toString(this.pos)+"], error: ["+e.getMessage()+"].", e);
future.complete(null);
future.completeExceptionally(e);
}
};
executor.execute(this.getAndBuildRenderDataRunnable);
@@ -202,6 +203,14 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
return false;
}
}
//=======================//
// Get LOD ID data //
// and build render data //
//=======================//
//region
@Nullable
private synchronized LodQuadBuilder getAndBuildRenderData()
{
@@ -215,7 +224,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
boolean enableTransparency = Config.Client.Advanced.Graphics.Quality.transparency.get().transparencyEnabled;
LodQuadBuilder lodQuadBuilder = new LodQuadBuilder(enableTransparency, this.level.getClientLevelWrapper());
LodQuadBuilder lodQuadBuilder = new LodQuadBuilder(enableTransparency, this.clientLevel.getClientLevelWrapper());
// get the adjacent positions
@@ -238,7 +247,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
// the render sources are only needed by this synchronous method,
// then they can be closed
ColumnRenderBufferBuilder.makeLodRenderData(lodQuadBuilder, thisRenderSource, this.level, adjacentRenderSections, adjIsSameDetailLevel);
ColumnRenderBufferBuilder.makeLodRenderData(lodQuadBuilder, thisRenderSource, this.clientLevel, adjacentRenderSections, adjIsSameDetailLevel);
return lodQuadBuilder;
}
catch (Exception e)
@@ -288,52 +297,63 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
detailLevel += DhSectionPos.SECTION_MINIMUM_DETAIL_LEVEL;
return detailLevel == DhSectionPos.getDetailLevel(this.pos);
}
private synchronized CompletableFuture<LodBufferContainer> uploadToGpuAsync(LodQuadBuilder lodQuadBuilder)
//endregion
private synchronized CompletableFuture<LodBufferContainer> uploadToGpuAsync(
CompletableFuture<Void> parentFuture,
LodQuadBuilder lodQuadBuilder)
{
CompletableFuture<LodBufferContainer> oldFuture = this.bufferUploadFutureRef.getAndSet(null);
if (oldFuture != null)
CompletableFuture<LodBufferContainer> uploadFuture = LodBufferContainer.tryMakeAndUploadBuffersAsync(this.pos, this.clientLevel, lodQuadBuilder);
uploadFuture.whenComplete((bufferContainer, e) ->
{
// canceling the previous future
// prevents the CPU from working on something that won't be used
oldFuture.cancel(true);
}
CompletableFuture<LodBufferContainer> future = ColumnRenderBufferBuilder.uploadBuffersAsync(this.level, this.pos, lodQuadBuilder);
future.handle((lodBufferContainer, throwable) ->
{
if (!this.bufferUploadFutureRef.compareAndSet(future, null)
// if the old future is canceled then the future ref will be different and that's expected
&& !future.isCancelled()
// if the old future is already done, then we don't care about the ref being swapped
&& !future.isDone())
try
{
LOGGER.warn("Buffer upload future ref changed for pos: ["+DhSectionPos.toString(this.pos)+"].");
// handle errors and early shutdown
if (e != null)
{
if (!ExceptionUtil.isShutdownException(e))
{
LOGGER.error("Unexpected issue uploading buffers for pos: [" + DhSectionPos.toString(this.pos) + "], error: [" + e.getMessage() + "].", e);
}
if (bufferContainer != null)
{
// shouldn't happen, but just in case
bufferContainer.close();
}
return;
}
// close the old container
LodBufferContainer oldContainer = this.renderBufferContainer;
this.renderBufferContainer = bufferContainer.buffersUploaded ? bufferContainer : null;
if (oldContainer != null)
{
oldContainer.close();
}
// upload complete
this.renderDataDirty = false;
if (parentFuture.isCancelled())
{
// if the parent future was canceled that likely means
// this LodRenderSection was closed before this point,
// meaning this buffer will become homeless,
// so we need to clean it up here
bufferContainer.close();
}
}
return null;
});
future.thenAccept((LodBufferContainer buffer) ->
{
// needed to clean up the old data
LodBufferContainer previousContainer = this.renderBufferContainer;
// upload complete
this.renderBufferContainer = buffer.buffersUploaded ? buffer : null;
if (previousContainer != null)
catch (Exception finishEx)
{
previousContainer.close();
LOGGER.error("Unexpected buffer finish exception: ["+finishEx.getMessage()+"]", finishEx);
}
});
if (!this.bufferUploadFutureRef.compareAndSet(null, future))
{
LodUtil.assertNotReach("Buffer upload future ref couldn't be set due to concurrency error, pos: ["+DhSectionPos.toString(this.pos)+"].");
}
return future;
return uploadFuture;
}
//endregion render data uploading
@@ -345,7 +365,14 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
//=================//
//region
public boolean gpuUploadComplete() { return this.renderBufferContainer != null; }
public boolean canRender() { return this.renderBufferContainer != null; }
public boolean gpuUploadComplete()
{
return this.renderBufferContainer != null
// render dirty is here so we can trigger new GPU uploads
// even if the render data is present
&& !this.renderDataDirty;
}
public boolean getRenderingEnabled() { return this.renderingEnabled; }
public void setRenderingEnabled(boolean enabled) { this.renderingEnabled = enabled;}
@@ -374,14 +401,14 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
{
color = Color.yellow;
}
else if (this.gpuUploadComplete())
else if (this.canRender())
{
//color = Color.cyan;
return;
}
int levelMinY = this.level.getLevelWrapper().getMinHeight();
int levelMaxY = this.level.getLevelWrapper().getMaxHeight();
int levelMinY = this.clientLevel.getLevelWrapper().getMinHeight();
int levelMaxY = this.clientLevel.getLevelWrapper().getMaxHeight();
// show the wireframe a bit lower than world max height,
// since most worlds don't render all the way up to the max height
@@ -418,12 +445,7 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
}
if (this.renderBufferContainer != null)
{
this.renderBufferContainer.close();
}
// removes any in-progress futures since they aren't needed any more
// render loading is no longer needed
CompletableFuture<Void> buildFuture = this.getAndBuildRenderDataFutureRef.get();
if (buildFuture != null)
{
@@ -439,12 +461,17 @@ public class LodRenderSection implements IDebugRenderable, AutoCloseable
renderLoaderExecutor.remove(runnable);
}
}
// cancel the future after removing the runnable
// to make sure the runnable is properly removed
buildFuture.cancel(true);
}
CompletableFuture<LodBufferContainer> uploadFuture = this.bufferUploadFutureRef.get();
if (uploadFuture != null)
this.setRenderingEnabled(false);
if (this.renderBufferContainer != null)
{
uploadFuture.cancel(true);
this.renderBufferContainer.close();
}
}
@@ -63,21 +63,39 @@ public class QuadTreeTickNodeHolder
{
if(this.presentNodes.add(node))
{
// not a big fan of having to check every node to prevent overlaps, but it does work
this.nodesToEnable.removeIf((QuadNode<LodRenderSection> checkNode) ->
// in James testing as of 4-21-2026
// this should no longer be needed to prevent overlaps,
// however I'm keeping it here as a quick fix solution if
// the problem comes up again
if (false)
{
boolean contained = DhSectionPos.contains(node.sectionPos, checkNode.sectionPos);
if (contained)
// not a big fan of having to check every node to prevent overlaps, but it does work
this.nodesToEnable.removeIf((QuadNode<LodRenderSection> checkNode) ->
{
this.nodesToDisable.add(checkNode);
}
return contained;
});
boolean contained = DhSectionPos.contains(node.sectionPos, checkNode.sectionPos);
if (contained)
{
this.nodesToDisable.add(checkNode);
}
return contained;
});
}
this.nodesToEnable.add(node);
}
}
/** */
public void removeEnableAndDisableNode(QuadNode<LodRenderSection> node)
{
this.nodesToEnable.remove(node);
this.nodesToEnableDeleteChildrenList.remove(node);
this.presentNodes.add(node); // should already be present, but re-added just in case
this.nodesToDisable.add(node); // node shouldn't be rendered since it's being disabled by a parent
}
public HashSet<QuadNode<LodRenderSection>> getEnabledNodes() { return this.nodesToEnable; }
@@ -6,18 +6,14 @@ import com.seibel.distanthorizons.core.api.internal.SharedApi;
import com.seibel.distanthorizons.core.api.internal.rendering.DhRenderState;
import com.seibel.distanthorizons.core.dependencyInjection.ModAccessorInjector;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.jar.EPlatform;
import com.seibel.distanthorizons.core.level.IDhClientLevel;
import com.seibel.distanthorizons.core.util.RenderUtil;
import com.seibel.distanthorizons.core.util.math.Mat4f;
import com.seibel.distanthorizons.core.util.math.Vec3d;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.world.IDhClientWorld;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.ILightMapWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor.AbstractOptifineAccessor;
import com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor.IOptifineAccessor;
import com.seibel.distanthorizons.core.wrapperInterfaces.render.renderPass.IDhGenericRenderer;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
@@ -87,7 +83,7 @@ public class RenderParams extends DhApiRenderParam
this.dhClientWorld = SharedApi.tryGetDhClientWorld();
if (this.dhClientWorld != null)
{
this.dhClientLevel = (IDhClientLevel) this.dhClientWorld.getLevel(clientLevelWrapper);
this.dhClientLevel = this.dhClientWorld.getOrLoadClientLevel(clientLevelWrapper);
if (this.dhClientLevel != null)
{
this.renderBufferHandler = this.dhClientLevel.getRenderBufferHandler();
@@ -176,45 +172,6 @@ public class RenderParams extends DhApiRenderParam
}
//// potential fix for a segfault when
//// Sodium and DH are running together
//if (EPlatform.get() == EPlatform.MACOS
// && !initialLoadingComplete)
//{
// // Once MC starts rendering, wait a few seconds so
// // MC/Sodium can finish their shader compiling before DH does its own.
// // This will allow DH to compile its own shaders after Sodium finishes
// // compiling its own.
// long nowMs = System.currentTimeMillis();
// long firstAllowedRenderTimeMs = firstRenderTimeMs + TIME_FOR_MAC_TO_FINISH_COMPILING_IN_MS;
// if (nowMs < firstAllowedRenderTimeMs)
// {
// return "Waiting for initial MC compile...";
// }
//
//
// // null shouldn't happen, but just in case
// PriorityTaskPicker.Executor renderLoadExecutor = ThreadPoolUtil.getRenderLoadingExecutor();
// if (renderLoadExecutor == null)
// {
// return "Waiting for DH Threadpool...";
// }
//
// // wait for DH to finish loading, by the time that's done
// // java should have finished all of DH's JIT compiling,
// // which will hopefully mean less concurrency and thus a lower
// // chance of breaking
// // (plus this gives Sodium/vanill a bit longer to finish their setup)
// int taskCount = renderLoadExecutor.getQueueSize();
// if (taskCount > 0)
// {
// return "Waiting for DH JIT compiling...";
// }
//
// initialLoadingComplete = true;
//}
return null;
}
@@ -10,6 +10,7 @@ import com.seibel.distanthorizons.core.util.TimerUtil;
import com.seibel.distanthorizons.core.util.objects.RollingAverage;
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.coreapi.ModInfo;
import org.jetbrains.annotations.Nullable;
@@ -49,6 +50,14 @@ public class RenderThreadTaskHandler
private long nanoSinceTasksRun = System.nanoTime();
private final boolean running;
private Thread renderThread;
/**
* the currently running {@link QueuedRunnable}
* will be null if nothing is running.
*/
private volatile @Nullable QueuedRunnable currentQueuedRunnable;
@@ -57,7 +66,22 @@ public class RenderThreadTaskHandler
//=============//
//region
private RenderThreadTaskHandler() { TIMER.scheduleAtFixedRate(TimerUtil.createTimerTask(this::manualCleanupTick), MS_BETWEEN_CLEANUP_TICKS, MS_BETWEEN_CLEANUP_TICKS); }
private RenderThreadTaskHandler()
{
// we only want to run this when the client is available
IMinecraftSharedWrapper mcShared = SingletonInjector.INSTANCE.get(IMinecraftSharedWrapper.class);
if (!mcShared.isDedicatedServer())
{
LOGGER.debug("Starting ["+RenderThreadTaskHandler.class.getSimpleName()+"]...");
this.running = true;
TIMER.scheduleAtFixedRate(TimerUtil.createTimerTask(this::manualCleanupTick), MS_BETWEEN_CLEANUP_TICKS, MS_BETWEEN_CLEANUP_TICKS);
}
else
{
this.running = false;
LOGGER.debug("Skipping ["+RenderThreadTaskHandler.class.getSimpleName()+"] startup due to running on a dedicated server.");
}
}
//endregion
@@ -70,6 +94,13 @@ public class RenderThreadTaskHandler
public void queueRunningOnRenderThread(String name, Runnable renderCall)
{
// don't queuing tasks if they'll never be run
if (!this.running)
{
return;
}
// don't get the stacktrace on release to reduce GC pressure
StackTraceElement[] stackTrace = null;
if (ModInfo.IS_DEV_BUILD)
@@ -116,12 +147,21 @@ public class RenderThreadTaskHandler
long loopStartTimeNano = System.nanoTime();
this.nanoSinceTasksRun = loopStartTimeNano;
if (this.renderThread == null)
{
this.renderThread = Thread.currentThread();
}
QueuedRunnable runnable = RENDER_THREAD_RUNNABLE_QUEUE.poll();
while(runnable != null)
{
long taskStartNano = System.nanoTime();
this.currentQueuedRunnable = runnable;
runnable.run();
this.currentQueuedRunnable = null;
// only try running for a limited amount of time to prevent lag spikes
long taskNano = System.nanoTime() - taskStartNano;
@@ -179,8 +219,19 @@ public class RenderThreadTaskHandler
// this means we could have GL jobs building up.
// Run the queued tasks on MC's executor (hopefully this should always run,
// even if DH's render code isn't being hit).
IMinecraftClientWrapper MC = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
MC.executeOnRenderThread(() -> this.runRenderThreadTasks(500 * 1_000_000L));
IMinecraftClientWrapper mcClient = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
if (mcClient != null)
{
mcClient.executeOnRenderThread(() -> this.runRenderThreadTasks(500 * 1_000_000L));
}
else
{
// shouldn't happen, but just in case
// somehow the timer started when there wasn't a client wrapper
// this probably means the timer was started on a dedicated server
RATE_LIMITED_LOGGER.warn("["+RenderThreadTaskHandler.class.getSimpleName()+"] timer started when ["+IMinecraftClientWrapper.class.getSimpleName()+"] is null. This shouldn't happen but can likely be ignored.");
}
}
//endregion
@@ -190,7 +241,7 @@ public class RenderThreadTaskHandler
//===========//
// debugging //
//===========//
///region
//region
/**
* if tasks are currently queued the debug
@@ -246,7 +297,28 @@ public class RenderThreadTaskHandler
});
}
///endregion
/** Returns true if the currently running thread is being run by this handler */
public boolean isCurrentThread()
{
if (this.renderThread != null)
{
return Thread.currentThread() == this.renderThread;
}
// shouldn't normally be needed, but can be used if this
// handler hasn't been run yet
return Thread.currentThread().getName().equals("Render thread");
}
/**
* Only recommended to be used by the task that's currently being run.
* Use {@link RenderThreadTaskHandler#isCurrentThread()} to check. <br>
* Can be used to get stack traces for render thread tasks while they're being run.
*/
public @Nullable QueuedRunnable getCurrentlyRunningTask() { return this.currentQueuedRunnable; }
//endregion
@@ -255,7 +327,7 @@ public class RenderThreadTaskHandler
//================//
//region
private static class QueuedRunnable implements Runnable
public static class QueuedRunnable implements Runnable
{
/** used to easily track what's being done on the render thread */
public final String name;
@@ -26,6 +26,7 @@ import com.seibel.distanthorizons.core.dependencyInjection.ModAccessorInjector;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.DhApiRenderProxy;
import com.seibel.distanthorizons.core.render.RenderBufferHandler;
import com.seibel.distanthorizons.core.render.RenderParams;
@@ -36,6 +37,8 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor.IIrisAccess
import com.seibel.distanthorizons.core.wrapperInterfaces.render.renderPass.*;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import java.awt.*;
/**
* This is where all the magic happens. <br>
* This is where LODs are draw to the world.
@@ -180,6 +183,24 @@ public class LodRenderer
renderBufferHandler.buildRenderList(renderParams);
}
boolean renderFog;
Boolean apiFogOverride = Config.Client.Advanced.Graphics.Fog.enableDhFog.getApiValue();
if (apiFogOverride != null)
{
// use whatever the API dictates if set
// (this could cause issues when underwater if a shader or something
// doesn't add their own, but that's relatively unlikely)
renderFog = apiFogOverride;
}
else
{
renderFog = Config.Client.Advanced.Graphics.Fog.enableDhFog.get();
// allow enabling fog when: underwater fog, blind, etc.
// otherwise LODs won't appear correctly
renderFog |= renderParams.vanillaFogEnabled;
}
//endregion
@@ -207,8 +228,6 @@ public class LodRenderer
// opaque LODs
profiler.popPush("LOD Opaque");
ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeRenderPassEvent.class, renderParams);
this.renderTerrain(this.terrainRenderer, renderBufferHandler, renderParams, /*opaquePass*/ true, profiler);
// custom objects with SSAO
@@ -249,9 +268,8 @@ public class LodRenderer
}
// fog
if (Config.Client.Advanced.Graphics.Fog.enableDhFog.get()
// this is done to fix issues with: underwater fog, blindness effect, etc.
|| renderParams.vanillaFogEnabled)
if (renderFog)
{
profiler.popPush("LOD Fog");
@@ -273,6 +291,22 @@ public class LodRenderer
}
if (Config.Client.Advanced.Debugging.PositionFinder.positionFinderEnable.get())
{
// can be used to find specific positions when debugging
this.debugWireframeRenderer.renderBox(new AbstractDebugWireframeRenderer.Box(
DhSectionPos.encode(
Config.Client.Advanced.Debugging.PositionFinder.positionFinderDetailLevel.get().byteValue(),
Config.Client.Advanced.Debugging.PositionFinder.positionFinderXPos.get(),
Config.Client.Advanced.Debugging.PositionFinder.positionFinderZPos.get()),
Config.Client.Advanced.Debugging.PositionFinder.positionFinderMinBlockY.get(),
Config.Client.Advanced.Debugging.PositionFinder.positionFinderMaxBlockY.get(),
Config.Client.Advanced.Debugging.PositionFinder.positionFinderMarginPercent.get(),
Color.GREEN
));
}
//=============================//
// Apply to the MC Framebuffer //
@@ -294,15 +328,11 @@ public class LodRenderer
if (Config.Client.Advanced.Graphics.Quality.transparency.get().transparencyEnabled)
{
ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeRenderPassEvent.class, renderParams);
profiler.popPush("LOD Transparent");
this.renderTerrain(this.terrainRenderer, renderBufferHandler, renderParams, /*opaquePass*/ false, profiler);
if (Config.Client.Advanced.Graphics.Fog.enableDhFog.get()
// this is done to fix issues with: underwater fog, blindness effect, etc.
|| renderParams.vanillaFogEnabled)
if (renderFog)
{
profiler.popPush("LOD Fog");
@@ -25,6 +25,7 @@ public class ExceptionUtil
return throwable instanceof InterruptedException
|| throwable instanceof UncheckedInterruptedException
|| throwable instanceof RejectedExecutionException
|| throwable instanceof CancellationException
|| throwable instanceof ClosedByInterruptException;
}
@@ -37,8 +38,8 @@ public class ExceptionUtil
unwrapped instanceof CancellationException;
}
public static Throwable ensureUnwrap(Throwable t)
{
return t instanceof CompletionException ? ensureUnwrap(t.getCause()) : t;
}
{ return t instanceof CompletionException ? ensureUnwrap(t.getCause()) : t; }
}
@@ -451,6 +451,21 @@ public class RenderDataPointReducingList extends AbstractPhantomArrayList
this.setBigger(smaller, bigger);
}
if (writeIndex == 0)
{
// if every data point in the list is NULL (0) the write index will be 0,
// and in order to prevent accessing index -1 below,
// setting the write index to 1 is needed.
// This shouldn't happen normally, however if the lod data is slightly malformed
// (which is specifically the case for the commonly shared wyncraft LODs)
// this check is needed.
// It would probably be best to fix the 6 or so NULL datapoints that are next
// to each other in the full data source, but for now this fix works.
writeIndex = 1;
}
this.smallest = this.sortingArray.getShort(0);
this.biggest = this.sortingArray.getShort(writeIndex - 1);
this.setSmaller(this.getSmallest(), NULL);
@@ -247,10 +247,7 @@ public class RenderDataPointUtil
{
return "Y+:" + getYMax(dataPoint) +
" Y-:" + getYMin(dataPoint) +
" argb:" + getAlpha(dataPoint) + " " +
getRed(dataPoint) + " " +
getGreen(dataPoint) + " " +
getBlue(dataPoint) +
" argb:" + getAlpha(dataPoint) + "," + getRed(dataPoint) + "," + getGreen(dataPoint) + "," + getBlue(dataPoint) +
" BL:" + getLightBlock(dataPoint) +
" SL:" + getLightSky(dataPoint) +
" MAT:" + getBlockMaterialId(dataPoint) + "["+ EDhApiBlockMaterial.getFromIndex(getBlockMaterialId(dataPoint))+"]";
@@ -123,7 +123,16 @@ public class RenderUtil
// At low render distances this hides the vanilla RD border
int chunkRenderDistance = MC_RENDER.getRenderDistance();
if (chunkRenderDistance <= 2)
if (IRIS_ACCESSOR != null
&& IRIS_ACCESSOR.isShaderPackInUse())
{
// shaders handle the near clip plane/overdraw differently, best to play it
// safe and have the plane really close otherwise
// there might be cutouts on the screen edges
overdraw = 0.2f;
}
else if (chunkRenderDistance <= 2)
{
overdraw = 0.2f;
}
@@ -154,7 +163,7 @@ public class RenderUtil
if (Config.Client.Advanced.Graphics.Culling.reduceOverdrawWithFastMovement.get())
{
double avgSpeed = ClientApi.INSTANCE.cameraSpeedRollingAverage.getAverage();
double avgSpeed = ClientApi.INSTANCE.getAvgCameraSpeed();
if (avgSpeed >= DynamicOverdraw.MIN_SPEED)
{
// if the player is moving fast enough,
@@ -331,7 +331,7 @@ public class PhantomArrayListPool
if (pool.logGarbageCollectedStacks
&& checkout.allocationStackTrace != null) // stack trace shouldn't be null, but just in case
{
putAndIncrementTrackingString(checkout.allocationStackTrace, allocationStackTraceCountPairList);
PhantomLoggingHelper.putAndIncrementTrackingString(checkout.allocationStackTrace, allocationStackTraceCountPairList);
}
}
else
@@ -363,18 +363,7 @@ public class PhantomArrayListPool
// log stack traces if present
if (pool.logGarbageCollectedStacks)
{
// high numbers first
allocationStackTraceCountPairList.sort((a, b) -> Integer.compare(b.second.get(), a.second.get()));
StringBuilder stringBuilder = new StringBuilder();
for (int j = 0; j < allocationStackTraceCountPairList.size(); j++)
{
int count = allocationStackTraceCountPairList.get(j).second.get();
String stack = allocationStackTraceCountPairList.get(j).first;
stringBuilder.append(count).append(". ").append(stack).append("\n");
}
LOGGER.warn("Stacks: ["+ allocationStackTraceCountPairList.size()+"]\n" + stringBuilder.toString());
PhantomLoggingHelper.LogAllocationStackTracePairCounts(LOGGER, allocationStackTraceCountPairList);
}
}
}
@@ -389,36 +378,6 @@ public class PhantomArrayListPool
}
}
}
/**
* This was separated out so it could be used for other string pair lists.
* James originally had an idea to add a shorter static string
* ID to each allocated {@link PhantomArrayListCheckout} as a simpler version of the stack trace,
* however it became a bit more difficult and messy than he wanted to deal with, so for now we just
* have the stack trace.
*/
private static void putAndIncrementTrackingString(
String key,
ArrayList<Pair<String, AtomicInteger>> allocationStackTraceCountPairList)
{
// sequential search, for the number of elements we're dealing with (less than 20)
// this should be sufficiently fast
boolean pairFound = false;
for (int i = 0; i < allocationStackTraceCountPairList.size(); i++)
{
Pair<String, AtomicInteger> possiblePair = allocationStackTraceCountPairList.get(i);
if (possiblePair.first.equals(key))
{
possiblePair.second.getAndIncrement();
pairFound = true;
break;
}
}
if (!pairFound)
{
allocationStackTraceCountPairList.add(new Pair<>(key, new AtomicInteger(1)));
}
}
///endregion
@@ -0,0 +1,232 @@
package com.seibel.distanthorizons.core.util.objects.pooling;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.util.ThreadUtil;
import com.seibel.distanthorizons.core.util.objects.Pair;
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicInteger;
public class PhantomLoggingHelper
{
/**
* This was separated out so it could be used for other string pair lists.
* James originally had an idea to add a shorter static string
* ID to each allocated {@link PhantomArrayListCheckout} as a simpler version of the stack trace,
* however it became a bit more difficult and messy than he wanted to deal with, so for now we just
* have the stack trace.
*/
public static void putAndIncrementTrackingString(
String key,
ArrayList<Pair<String, AtomicInteger>> allocationStackTraceCountPairList)
{
// sequential search, for the number of elements we're dealing with (less than 20)
// this should be sufficiently fast
boolean pairFound = false;
for (int i = 0; i < allocationStackTraceCountPairList.size(); i++)
{
Pair<String, AtomicInteger> possiblePair = allocationStackTraceCountPairList.get(i);
if (possiblePair.first.equals(key))
{
possiblePair.second.getAndIncrement();
pairFound = true;
break;
}
}
if (!pairFound)
{
allocationStackTraceCountPairList.add(new Pair<>(key, new AtomicInteger(1)));
}
}
public static void LogAllocationStackTracePairCounts(DhLogger logger, ArrayList<Pair<String, AtomicInteger>> allocationStackTraceCountPairList)
{
// high numbers first
allocationStackTraceCountPairList.sort((a, b) -> Integer.compare(b.second.get(), a.second.get()));
StringBuilder stringBuilder = new StringBuilder();
for (int j = 0; j < allocationStackTraceCountPairList.size(); j++)
{
int count = allocationStackTraceCountPairList.get(j).second.get();
String stack = allocationStackTraceCountPairList.get(j).first;
stringBuilder.append(count).append(". ").append(stack).append("\n");
}
logger.warn("Stacks: ["+ allocationStackTraceCountPairList.size()+"]\n" + stringBuilder.toString());
}
//================//
// helper classes //
//================//
//region
/**
* Can be quickly added to a {@link AutoCloseable} implementing
* class to confirm it's being properly closed.
*/
public static class BasicPhantomReference implements AutoCloseable
{
private static final DhLogger LOGGER = new DhLoggerBuilder().build();
/** if enabled the number of GC'ed buffers will be logged */
private static final boolean LOG_PHANTOM_RECOVERY = true;
/**
* If enabled the GC'ed buffers allocation/upload stacks will be logged.
* Note: due to how the buffers are often run on the render thread,
* these stacks will likely only be of limited use.
* For more robust debugging it would likely be best to somehow track
* the stacks of where these calls are happening before they're queued
* for the render thread.
*/
private static final boolean LOG_PHANTOM_ALLOCATION_STACKS = true;
private static final int PHANTOM_REF_CHECK_TIME_IN_MS = 5 * 1000;
private static final ReferenceQueue<BasicPhantomReference> PHANTOM_REFERENCE_QUEUE = new ReferenceQueue<>();
private static final ConcurrentHashMap<PhantomReference<? extends BasicPhantomReference>, Class<?>> PHANTOM_TO_PARENT_CLASS = new ConcurrentHashMap<>();
private static final ThreadPoolExecutor CLEANUP_THREAD = ThreadUtil.makeSingleDaemonThreadPool("BasicPhantom Cleanup");
private final Class<?> parentClass;
private final PhantomReference<? extends BasicPhantomReference> phantomReference;
//==============//
// constructors //
//==============//
//region
static { CLEANUP_THREAD.execute(() -> runPhantomReferenceCleanupLoop()); }
public BasicPhantomReference(Class<?> parentClass)
{
this.parentClass = parentClass;
this.phantomReference = new PhantomReference<>(this, PHANTOM_REFERENCE_QUEUE);
PHANTOM_TO_PARENT_CLASS.put(this.phantomReference, this.parentClass);
}
//endregion
//================//
// base overrides //
//================//
//region
@Override
public void close()
{
this.phantomReference.clear();
PHANTOM_TO_PARENT_CLASS.remove(this.phantomReference);
}
//endregion
//================//
// static cleanup //
//================//
//region
private static void runPhantomReferenceCleanupLoop()
{
// these arrays are stored here so they don't have to be re-allocated each loop
ArrayList<Pair<String, AtomicInteger>> allocationStackTraceCountPairList = new ArrayList<>();
ArrayList<Pair<String, AtomicInteger>> parentClassNameCountPairList = new ArrayList<>();
while (true)
{
allocationStackTraceCountPairList.clear();
parentClassNameCountPairList.clear();
try
{
try
{
Thread.sleep(PHANTOM_REF_CHECK_TIME_IN_MS);
}
catch (InterruptedException ignore) { }
int collectedCount = 0;
Reference<? extends BasicPhantomReference> phantomRef = PHANTOM_REFERENCE_QUEUE.poll();
while (phantomRef != null)
{
// destroy the buffer if it hasn't been cleared yet
Class<?> parentClass = PHANTOM_TO_PARENT_CLASS.remove((PhantomReference<? extends BasicPhantomReference>)phantomRef); // cast to make IntelliJ happy
{
String parentClassName = "NULL";
if (parentClass != null)
{
parentClassName = parentClass.getSimpleName();
}
PhantomLoggingHelper.putAndIncrementTrackingString(parentClassName, parentClassNameCountPairList);
//LOGGER.info("Phantom collected for class: [" + parentClassName + "]");
}
//if (LOG_PHANTOM_ALLOCATION_STACKS) // stack trace shouldn't be null, but just in case
//{
// String stack = BUFFER_ID_TO_ALLOCATION_STRING.get(idRef);
// if (stack != null)
// {
// PhantomLoggingHelper.putAndIncrementTrackingString(stack, allocationStackTraceCountPairList);
// }
//}
collectedCount++;
phantomRef = PHANTOM_REFERENCE_QUEUE.poll();
}
if (LOG_PHANTOM_RECOVERY)
{
// we only want to log when something has been returned
if (collectedCount != 0)
{
LOGGER.warn("Phantoms collected: ["+ F3Screen.NUMBER_FORMAT.format(collectedCount)+"].");
PhantomLoggingHelper.LogAllocationStackTracePairCounts(LOGGER, parentClassNameCountPairList);
//// log stack traces if present
//if (LOG_PHANTOM_ALLOCATION_STACKS)
//{
// PhantomLoggingHelper.LogAllocationStackTracePairCounts(LOGGER, allocationStackTraceCountPairList);
//}
}
}
}
catch (Exception e)
{
LOGGER.error("Unexpected error in buffer cleanup thread: [" + e.getMessage() + "].", e);
}
}
}
//endregion
}
//endregion
}
@@ -325,7 +325,7 @@ public class QuadNode<T>
public void deleteAllChildren() { this.deleteAllChildren(null); }
/** @param removedItemConsumer is only fired for non-null nodes, however the value passed in may be null */
public void deleteAllChildren(Consumer<? super T> removedItemConsumer)
public void deleteAllChildren(@Nullable Consumer<? super T> removedItemConsumer)
{
for (int i = 0; i < 4; i++)
{
@@ -19,10 +19,12 @@
package com.seibel.distanthorizons.core.util.objects.quadTree;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.render.QuadTree.LodQuadTree;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import com.seibel.distanthorizons.coreapi.util.MathUtil;
@@ -61,6 +63,11 @@ public class QuadTree<T>
private final MovableGridRingList<QuadNode<T>> topRingList;
private DhBlockPos2D centerBlockPos;
/**
* defines how many blocks the center needs to move in blocks
* before we check for out-of-bound nodes.
*/
private int blockDistanceForNodeClearing;
@@ -74,10 +81,13 @@ public class QuadTree<T>
*
* @param diameterInBlocks equivalent to the distance between the two opposing sides
*/
public QuadTree(int diameterInBlocks, DhBlockPos2D centerBlockPos, byte treeLeafDetailLevel)
public QuadTree(
int diameterInBlocks, int blockDistanceForNodeClearing,
DhBlockPos2D centerBlockPos, byte treeLeafDetailLevel)
{
this.centerBlockPos = centerBlockPos;
this.diameterInBlocks = diameterInBlocks;
this.blockDistanceForNodeClearing = blockDistanceForNodeClearing;
this.treeLeafDetailLevel = treeLeafDetailLevel;
// the min detail level must be greater than 0 (to prevent divide by 0 errors) and greater than the maximum detail level
@@ -130,18 +140,22 @@ public class QuadTree<T>
public int leafNodeCount()
{
int count = 0;
for (QuadNode<T> node : this.topRingList)
for (QuadNode<T> rootNode : this.topRingList)
{
if (node == null)
if (rootNode == null)
{
continue;
}
Iterator<QuadNode<T>> leafNodeIterator = node.getLeafNodeIterator();
Iterator<QuadNode<T>> leafNodeIterator = rootNode.getLeafNodeIterator();
while (leafNodeIterator.hasNext())
{
leafNodeIterator.next();
count++;
QuadNode<T> node = leafNodeIterator.next();
if (node != null
&& this.isSectionPosInBounds(node.sectionPos))
{
count++;
}
}
}
@@ -236,32 +250,32 @@ public class QuadTree<T>
int ringListPosX = DhSectionPos.getX(rootPos);
int ringListPosZ = DhSectionPos.getZ(rootPos);
QuadNode<T> topQuadNode = this.topRingList.get(ringListPosX, ringListPosZ);
if (topQuadNode == null)
QuadNode<T> rootQuadNode = this.topRingList.get(ringListPosX, ringListPosZ);
if (rootQuadNode == null)
{
if (!setNewValue)
{
return null;
}
topQuadNode = new QuadNode<T>(rootPos, this.treeLeafDetailLevel);
boolean successfullyAdded = this.topRingList.set(ringListPosX, ringListPosZ, topQuadNode);
rootQuadNode = new QuadNode<T>(rootPos, this.treeLeafDetailLevel);
boolean successfullyAdded = this.topRingList.set(ringListPosX, ringListPosZ, rootQuadNode);
if (!successfullyAdded)
{
LodUtil.assertNotReach("Failed to add top quadTree node at position: " + rootPos);
LodUtil.assertNotReach("Failed to add root quadTree node at position: ["+DhSectionPos.toString(rootPos)+"]");
}
}
if (!DhSectionPos.contains(topQuadNode.sectionPos, pos))
if (!DhSectionPos.contains(rootQuadNode.sectionPos, pos))
{
LodUtil.assertNotReach("failed to get a root node that contains the input position: " + pos + " root node pos: " + topQuadNode.sectionPos);
LodUtil.assertNotReach("failed to get a root node that contains the input position: " + pos + " root node pos: " + rootQuadNode.sectionPos);
}
QuadNode<T> returnNode = topQuadNode.getNode(pos);
QuadNode<T> returnNode = rootQuadNode.getNode(pos);
if (setNewValue)
{
topQuadNode.setValue(pos, newValue);
rootQuadNode.setValue(pos, newValue);
}
return returnNode;
}
@@ -354,32 +368,103 @@ public class QuadTree<T>
//================//
//region
public void setCenterBlockPos(DhBlockPos2D newCenterPos) { this.setCenterBlockPos(newCenterPos, null); }
public void setCenterBlockPos(DhBlockPos2D newCenterPos, Consumer<? super T> removedItemConsumer)
public void setCenterBlockPos(DhBlockPos2D newCenterPos) { this.setCenterBlockPos(newCenterPos, null, null); }
/**
* @param removedConsumer fired when a root node is completely removed from the underlying data structure
* @param mutateOutOfBoundConsumer fired when a child node is out of bounds, but not removed from the underlying data structure
*/
public void setCenterBlockPos(
DhBlockPos2D newCenterPos,
@Nullable Consumer<? super T> removedConsumer,
@Nullable Consumer<? super T> mutateOutOfBoundConsumer)
{
this.centerBlockPos = newCenterPos;
MovableGridRingList.Pos2D expectedCenterPos = new MovableGridRingList.Pos2D(
BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.x, this.treeRootDetailLevel),
BitShiftUtil.divideByPowerOfTwo(this.centerBlockPos.z, this.treeRootDetailLevel));
if (this.topRingList.getCenter().equals(expectedCenterPos))
// did we move significantly?
boolean ringListMoved = false;
int newCenterPosX = BitShiftUtil.divideByPowerOfTwo(newCenterPos.x, this.treeRootDetailLevel);
int newCenterPosZ = BitShiftUtil.divideByPowerOfTwo(newCenterPos.z, this.treeRootDetailLevel);
if (this.topRingList.getCenter().getX() != newCenterPosX
|| this.topRingList.getCenter().getY() != newCenterPosZ)
{
// tree doesn't need to be moved
ringListMoved = true;
}
// did we move a little bit?
boolean recalculateOutOfBoundNodes = false;
int centerBlockDistance = this.centerBlockPos.manhattanDist(newCenterPos);
if (centerBlockDistance >= this.blockDistanceForNodeClearing)
{
recalculateOutOfBoundNodes = true;
}
if (!ringListMoved
&& !recalculateOutOfBoundNodes)
{
// the tree didn't move enough that we need
// to re-calculate anything
return;
}
// remove out of bounds root nodes
this.topRingList.moveTo(expectedCenterPos.getX(), expectedCenterPos.getY(), (quadNode) ->
this.centerBlockPos = newCenterPos;
// remove out of bound root nodes
this.topRingList.moveTo(newCenterPosX, newCenterPosZ, (quadNode) ->
{
if (quadNode != null && removedItemConsumer != null)
if (quadNode != null)
{
quadNode.deleteAllChildren(removedItemConsumer);
quadNode.deleteAllChildren(removedConsumer);
removedItemConsumer.accept(quadNode.value);
if (removedConsumer != null)
{
removedConsumer.accept(quadNode.value);
}
}
});
// mutate out of bound child nodes
this.topRingList.forEach((rootNode) ->
{
this.mutateOutOfBoundChildNodes(rootNode, mutateOutOfBoundConsumer);
});
}
/**
* we don't want to actually remove nodes or node data
* since that can cause the {@link LodQuadTree} to
* flash low-detail LODs when moving into previously-loaded
* LODs, which is really disorienting.
*/
private void mutateOutOfBoundChildNodes(@Nullable QuadNode<T> quadNode, @Nullable Consumer<? super T> mutateOutOfBoundConsumer)
{
// nodes shouldn't be null, but just in case
if (quadNode == null)
{
return;
}
// go over each child node
for (int i = 0; i < 4; i++)
{
QuadNode<T> childNode = quadNode.getChildByIndex(i);
if (childNode == null
|| childNode.value == null)
{
// no need to go any deeper if this node is already empty
continue;
}
// mutate nodes from the bottom up
this.mutateOutOfBoundChildNodes(childNode, mutateOutOfBoundConsumer);
// mutate this node if out of bounds
if (!this.isSectionPosInBounds(childNode.sectionPos))
{
if (mutateOutOfBoundConsumer != null)
{
mutateOutOfBoundConsumer.accept(childNode.value);
}
}
}
}
public final DhBlockPos2D getCenterBlockPos() { return this.centerBlockPos; }
@@ -484,7 +569,7 @@ public class QuadTree<T>
private class QuadTreeNodeIterator implements Iterator<QuadNode<T>>
{
private final QuadTreeRootPosIterator rootNodeIterator;
private final QuadTreeRootPosIterator rootNodePosIterator;
private Iterator<QuadNode<T>> currentNodeIterator;
private QuadNode<T> lastNode = null;
@@ -497,7 +582,7 @@ public class QuadTree<T>
public QuadTreeNodeIterator(boolean onlyReturnLeaves, @Nullable INodeIteratorStoppingFunc<T> stopIteratingFunc)
{
this.rootNodeIterator = new QuadTreeRootPosIterator(false, stopIteratingFunc);
this.rootNodePosIterator = new QuadTreeRootPosIterator(false, stopIteratingFunc);
this.onlyReturnLeaves = onlyReturnLeaves;
this.stopIteratingFunc = stopIteratingFunc;
@@ -508,7 +593,7 @@ public class QuadTree<T>
@Override
public boolean hasNext()
{
if (!this.rootNodeIterator.hasNext()
if (!this.rootNodePosIterator.hasNext()
&& this.currentNodeIterator != null
&& !this.currentNodeIterator.hasNext())
{
@@ -544,9 +629,9 @@ public class QuadTree<T>
{
Iterator<QuadNode<T>> nodeIterator = null;
while ((nodeIterator == null || !nodeIterator.hasNext())
&& this.rootNodeIterator.hasNext())
&& this.rootNodePosIterator.hasNext())
{
long sectionPos = this.rootNodeIterator.nextLong();
long sectionPos = this.rootNodePosIterator.nextLong();
// try-get to prevent concurrency errors if the tree is being moved while we walk through it
QuadNode<T> rootNode = QuadTree.this.tryGetNode(sectionPos);
@@ -73,10 +73,17 @@ public class RateLimitedThreadPoolExecutor extends ThreadPoolExecutor
{
super.afterExecute(runnable, throwable);
double ratio = this.runTimeRatioConfig.get();
if (ratio >= 1.0)
{
// Avoid sleeping for 0 time
return;
}
try
{
long runTime = System.nanoTime() - this.runStartTime.get();
Thread.sleep(TimeUnit.NANOSECONDS.toMillis((long) (runTime / this.runTimeRatioConfig.get() - runTime)));
Thread.sleep(TimeUnit.NANOSECONDS.toMillis((long) (runTime / ratio - runTime)));
}
catch (InterruptedException ignore)
{
@@ -170,7 +170,7 @@ public class ThreadPoolUtil
*/
public static boolean worldGenThreadsCanRun()
{
double cameraSpeed = ClientApi.INSTANCE.cameraSpeedRollingAverage.getAverage();
double cameraSpeed = ClientApi.INSTANCE.getAvgCameraSpeed();
// stop these threads if moving a little bit slower than max elytra speed
double maxAllowedSpeed = (LodUtil.ROCKET_ELYTRA_SPEED_IN_BLOCKS_PER_SEC - 10.0);
if (cameraSpeed > maxAllowedSpeed)
@@ -1,5 +1,6 @@
package com.seibel.distanthorizons.core.world;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelUnloadEvent;
import com.seibel.distanthorizons.core.file.structure.LocalSaveStructure;
import com.seibel.distanthorizons.core.level.AbstractDhServerLevel;
import com.seibel.distanthorizons.core.level.IDhLevel;
@@ -8,6 +9,7 @@ import com.seibel.distanthorizons.core.multiplayer.server.ServerPlayerStateManag
import com.seibel.distanthorizons.core.wrapperInterfaces.misc.IServerPlayerWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
@@ -138,6 +140,7 @@ public abstract class AbstractDhServerWorld<TDhServerLevel extends AbstractDhSer
if (serverLevelWrapper != null)
{
serverLevelWrapper.onUnload();
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(serverLevelWrapper));
}
@@ -64,7 +64,7 @@ public abstract class AbstractDhWorld implements IDhWorld, Closeable
String levelCountStr = F3Screen.NUMBER_FORMAT.format(this.getLoadedLevelCount());
String readOnlyStr = "";
if (DhApiWorldProxy.INSTANCE.getReadOnly())
if (DhApiWorldProxy.INSTANCE.tryGetReadOnly())
{
readOnlyStr += " - ReadOnly";
}
@@ -113,6 +113,23 @@ public class DhApiWorldProxy implements IDhApiWorldProxy
return this.isReadOnly;
}
/**
* Returns false if no world is loaded.
* Can be used in places where the world state might be a bit more questionable
* without having to worry about the {@link IllegalStateException} thrown by
* {@link DhApiWorldProxy#getReadOnly()}
*/
public boolean tryGetReadOnly()
{
if (SharedApi.getAbstractDhWorld() == null)
{
// no world is loaded, use the default value for the next world
return false;
}
return this.isReadOnly;
}
//================//
@@ -19,6 +19,8 @@
package com.seibel.distanthorizons.core.world;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelLoadEvent;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelUnloadEvent;
import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.enums.MinecraftTextFormat;
import com.seibel.distanthorizons.core.level.DhClientServerLevel;
@@ -27,6 +29,7 @@ import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import org.jetbrains.annotations.NotNull;
import java.util.*;
@@ -34,7 +37,7 @@ import java.util.concurrent.CompletableFuture;
public class DhClientServerWorld extends AbstractDhServerWorld<DhClientServerLevel> implements IDhClientWorld
{
private final Set<DhClientServerLevel> dhLevels = Collections.synchronizedSet(new HashSet<>());
private final Map<DhClientServerLevel, Set<ILevelWrapper>> dhLevels = Collections.synchronizedMap(new HashMap<>());
private final Timer clientTickTimer = TimerUtil.CreateTimer("ClientTickTimer");
@@ -54,7 +57,7 @@ public class DhClientServerWorld extends AbstractDhServerWorld<DhClientServerLev
@Override
public void run()
{
DhClientServerWorld.this.dhLevels.forEach(DhClientServerLevel::clientTick);
DhClientServerWorld.this.dhLevels.keySet().forEach(DhClientServerLevel::clientTick);
}
}, 0, IDhClientWorld.TICK_RATE_IN_MS);
}
@@ -75,7 +78,8 @@ public class DhClientServerWorld extends AbstractDhServerWorld<DhClientServerLev
try
{
DhClientServerLevel level = new DhClientServerLevel(this.saveStructure, (IServerLevelWrapper) levelWrapper, this.getServerPlayerStateManager());
this.dhLevels.add(level);
this.dhLevels.computeIfAbsent(level, k -> Collections.synchronizedSet(new HashSet<>()));
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(wrapper));
return level;
}
catch (Exception e)
@@ -92,6 +96,10 @@ public class DhClientServerWorld extends AbstractDhServerWorld<DhClientServerLev
}
else
{
if (wrapper instanceof IClientLevelWrapper)
{
((IClientLevelWrapper) wrapper).markAccessed();
}
return this.dhLevelByLevelWrapper.computeIfAbsent(wrapper, (levelWrapper) ->
{
IClientLevelWrapper clientLevelWrapper = (IClientLevelWrapper) levelWrapper;
@@ -111,13 +119,14 @@ public class DhClientServerWorld extends AbstractDhServerWorld<DhClientServerLev
level.startRenderer();
clientLevelWrapper.setDhLevel(level);
dhLevels.get(level).add(wrapper);
return level;
});
}
}
@Override
public void unloadLevel(@NotNull ILevelWrapper wrapper)
public boolean unloadLevel(@NotNull ILevelWrapper wrapper)
{
if (this.dhLevelByLevelWrapper.containsKey(wrapper))
{
@@ -135,9 +144,18 @@ public class DhClientServerWorld extends AbstractDhServerWorld<DhClientServerLev
// If the level wrapper is a Client Level Wrapper, then that means the client side leaves the level,
// but note that the server side still has the level loaded. So, we don't want to unload the level,
// we just want to stop rendering it.
this.dhLevelByLevelWrapper.remove(wrapper).stopRenderer(); // Ignore resource warning. The level obj is referenced elsewhere.
DhClientServerLevel level = this.dhLevelByLevelWrapper.remove(wrapper); // Ignore resource warning. The level obj is referenced elsewhere.
Set<ILevelWrapper> wrappers = dhLevels.get(level);
if (wrappers != null) wrappers.remove(wrapper);
if ((wrappers == null || wrappers.isEmpty()) && level.isRendering()) {
level.stopRenderer();
}
wrapper.onUnload(); // We still want to unload the wrapper though.
}
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(wrapper));
return true;
}
return false;
}
@@ -155,13 +173,21 @@ public class DhClientServerWorld extends AbstractDhServerWorld<DhClientServerLev
synchronized (this.dhLevels)
{
// close each level
for (DhClientServerLevel level : this.dhLevels)
for (DhClientServerLevel level : this.dhLevels.keySet())
{
// level wrapper shouldn't be null, but just in case
IServerLevelWrapper serverLevelWrapper = level.getServerLevelWrapper();
if (serverLevelWrapper != null)
{
serverLevelWrapper.onUnload();
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(serverLevelWrapper));
}
IClientLevelWrapper clientLevelWrapper = level.getClientLevelWrapper();
if (clientLevelWrapper != null)
{
clientLevelWrapper.onUnload();
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(clientLevelWrapper));
}
// close levels asynchronously to speed up
@@ -19,6 +19,8 @@
package com.seibel.distanthorizons.core.world;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelLoadEvent;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelUnloadEvent;
import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.enums.MinecraftTextFormat;
import com.seibel.distanthorizons.core.file.structure.ClientOnlySaveStructure;
@@ -28,18 +30,17 @@ import com.seibel.distanthorizons.core.multiplayer.client.ClientNetworkState;
import com.seibel.distanthorizons.core.util.TimerUtil;
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.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld
{
private final ConcurrentHashMap<IClientLevelWrapper, DhClientLevel> levels;
private final ConcurrentHashMap<String, DhClientLevel> levels;
private final Map<String, Set<IClientLevelWrapper>> levelWrappers = new ConcurrentHashMap<>();
public final ClientOnlySaveStructure saveStructure;
public final ClientNetworkState networkState = new ClientNetworkState();
@@ -76,6 +77,32 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld
// methods //
//=========//
private DhClientLevel createClientLevel(@NotNull IClientLevelWrapper clientLevelWrapper) {
try
{
if (!ClientApi.INSTANCE.canLoadClientLevel(clientLevelWrapper))
{
return null;
}
DhClientLevel level = new DhClientLevel(this.saveStructure, clientLevelWrapper, this.networkState);
levelWrappers.computeIfAbsent(clientLevelWrapper.getDhIdentifier(), k -> Collections.synchronizedSet(new HashSet<>())).add(clientLevelWrapper);
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(clientLevelWrapper));
ClientApi.INSTANCE.loadWaitingChunksForLevel(clientLevelWrapper);
return level;
}
catch (Exception e)
{
LOGGER.fatal("Failed to load client level, error: ["+e.getMessage()+"].", e);
ClientApi.INSTANCE.showChatMessageNextFrame(
MinecraftTextFormat.RED + "Distant Horizons: Client level loading failed." + MinecraftTextFormat.CLEAR_FORMATTING + "\n" +
"Unable to load level ["+clientLevelWrapper.getDhIdentifier()+"], LODs may not appear. See log for more information.");
return null;
}
}
@Override
public DhClientLevel getOrLoadLevel(@NotNull ILevelWrapper wrapper)
{
@@ -83,25 +110,20 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld
{
return null;
}
return this.levels.computeIfAbsent((IClientLevelWrapper) wrapper,
(clientLevelWrapper) ->
IClientLevelWrapper clientLevelWrapper = (IClientLevelWrapper) wrapper;
clientLevelWrapper.markAccessed();
DhClientLevel storedLevel = this.levels.computeIfAbsent(wrapper.getDhIdentifier(),
(key) -> createClientLevel(clientLevelWrapper)
);
if (storedLevel != null && storedLevel.getClientLevelWrapper() != wrapper) {
unloadLevel(storedLevel.getLevelWrapper());
storedLevel = createClientLevel(clientLevelWrapper);
if (storedLevel != null)
{
try
{
return new DhClientLevel(this.saveStructure, clientLevelWrapper, this.networkState);
}
catch (Exception e)
{
LOGGER.fatal("Failed to load client level, error: ["+e.getMessage()+"].", e);
ClientApi.INSTANCE.showChatMessageNextFrame(
MinecraftTextFormat.RED + "Distant Horizons: Client level loading failed." + MinecraftTextFormat.CLEAR_FORMATTING + "\n" +
"Unable to load level ["+clientLevelWrapper.getDhIdentifier()+"], LODs may not appear. See log for more information.");
return null;
}
});
this.levels.put(wrapper.getDhIdentifier(), storedLevel);
}
}
return storedLevel;
}
@Override
@@ -112,7 +134,7 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld
return null;
}
return this.levels.get(wrapper);
return this.levels.get(wrapper.getDhIdentifier());
}
@Override
@@ -121,19 +143,26 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld
public int getLoadedLevelCount() { return this.levels.size(); }
@Override
public void unloadLevel(@NotNull ILevelWrapper wrapper)
public boolean unloadLevel(@NotNull ILevelWrapper wrapper)
{
if (!(wrapper instanceof IClientLevelWrapper))
{
return;
return false;
}
if (this.levels.containsKey(wrapper))
if (this.levels.containsKey(wrapper.getDhIdentifier()))
{
LOGGER.info("Unloading level " + this.levels.get(wrapper));
LOGGER.info("Unloading level " + this.levels.get(wrapper.getDhIdentifier()));
wrapper.onUnload();
this.levels.remove(wrapper).close();
Set<IClientLevelWrapper> wrappers = this.levelWrappers.get(wrapper.getDhIdentifier());
wrappers.remove(wrapper);
if (wrappers.isEmpty()) {
this.levels.remove(wrapper.getDhIdentifier()).close();
}
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(wrapper));
return true;
}
return false;
}
@Override
@@ -156,6 +185,7 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld
if (clientLevelWrapper != null)
{
clientLevelWrapper.onUnload();
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(clientLevelWrapper));
}
@@ -178,6 +208,7 @@ public class DhClientWorld extends AbstractDhWorld implements IDhClientWorld
}
this.levels.clear();
this.levelWrappers.clear();
this.clientTickTimer.cancel();
LOGGER.info("Closed DhWorld of type [" + this.environment + "].");
}
@@ -19,12 +19,15 @@
package com.seibel.distanthorizons.core.world;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelLoadEvent;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiLevelUnloadEvent;
import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.enums.MinecraftTextFormat;
import com.seibel.distanthorizons.core.generation.PregenManager;
import com.seibel.distanthorizons.core.level.DhServerLevel;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
import com.seibel.distanthorizons.coreapi.DependencyInjection.ApiEventInjector;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.CompletableFuture;
@@ -64,7 +67,9 @@ public class DhServerWorld extends AbstractDhServerWorld<DhServerLevel>
{
try
{
return new DhServerLevel(this.saveStructure, (IServerLevelWrapper) serverLevelWrapper, this.getServerPlayerStateManager());
DhServerLevel level = new DhServerLevel(this.saveStructure, (IServerLevelWrapper) serverLevelWrapper, this.getServerPlayerStateManager());
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelLoadEvent.class, new DhApiLevelLoadEvent.EventParam(wrapper));
return level;
}
catch (Exception e)
{
@@ -80,19 +85,21 @@ public class DhServerWorld extends AbstractDhServerWorld<DhServerLevel>
}
@Override
public void unloadLevel(@NotNull ILevelWrapper wrapper)
public boolean unloadLevel(@NotNull ILevelWrapper wrapper)
{
if (!(wrapper instanceof IServerLevelWrapper))
{
return;
return false;
}
if (this.dhLevelByLevelWrapper.containsKey(wrapper))
{
DhServerLevel level = this.dhLevelByLevelWrapper.get(wrapper);
wrapper.onUnload();
this.dhLevelByLevelWrapper.remove(wrapper).close();
ApiEventInjector.INSTANCE.fireAllEvents(DhApiLevelUnloadEvent.class, new DhApiLevelUnloadEvent.EventParam(wrapper));
return true;
}
return false;
}
@Override
@@ -49,6 +49,6 @@ public interface IDhWorld extends Closeable
Iterable<? extends IDhLevel> getAllLoadedLevels();
int getLoadedLevelCount();
void unloadLevel(@NotNull ILevelWrapper levelWrapper);
boolean unloadLevel(@NotNull ILevelWrapper levelWrapper);
}
@@ -96,8 +96,6 @@ public interface IMinecraftRenderWrapper extends IBindable
@Nullable
ILightMapWrapper getLightmapWrapper(@NotNull ILevelWrapper level);
float getShade(EDhDirection lodDirection);
}
@@ -19,6 +19,7 @@
package com.seibel.distanthorizons.core.wrapperInterfaces.minecraft;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IServerLevelWrapper;
import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable;
import java.io.File;
@@ -31,6 +32,6 @@ public interface IMinecraftSharedWrapper extends IBindable
int getPlayerCount();
IServerLevelWrapper getWrappedServerLevel(String levelKey);
}
@@ -0,0 +1,47 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.util.math.Vec3d;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import org.jetbrains.annotations.Nullable;
public interface IImmersivePortalsAccessor extends IModAccessor
{
boolean isRenderingPortal();
boolean wasPortalRecentlyVisible();
@Nullable
DhBlockPos getOriginalPlayerBlockPos();
@Nullable
DhChunkPos getOriginalPlayerChunkPos();
@Nullable
IClientLevelWrapper getOriginalClientLevelWrapper();
@Nullable
Vec3d getOriginalCameraPos();
}
@@ -0,0 +1,369 @@
/*
* This file is part of the Distant Horizons mod
* licensed under the GNU LGPL v3 License.
*
* Copyright (C) 2020 James Seibel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.wrapperInterfaces.modAccessor;
import com.seibel.distanthorizons.api.DhApi;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiBeforeRenderEvent;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiCancelableEventParam;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import org.jetbrains.annotations.NotNull;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.function.Supplier;
public abstract class ImmersivePortalsAbstractAccessor implements IImmersivePortalsAccessor
{
private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static Class<?> portalClass;
private static MethodHandle isRenderingMethodHandle;
private static Method shouldSkipRenderingPortalMethod;
private static MethodHandle getGlobalPortalsMethodHandle;
private static long lastPortalMsTime = -1;
private static boolean portalVisible = false;
//=============//
// constructor //
//=============//
//region
public ImmersivePortalsAbstractAccessor()
{
LOGGER.info("Immersive Portals detected: some DH features will be disabled or may only partially function.");
BeforeRenderEvent event = new BeforeRenderEvent(this);
DhApi.events.bind(DhApiBeforeRenderEvent.class, event);
}
//endregion
//=====================//
// reflection handling //
//=====================//
//region
private static Class<?> getPortalRenderingClass()
{
try
{
return Class.forName("qouteall.imm_ptl.core.render.context_management.PortalRendering");
}
catch (ClassNotFoundException first)
{
try
{
return Class.forName("com.qouteall.immersive_portals.render.context_management.PortalRendering"); // 1.16
}
catch (ClassNotFoundException second)
{
RuntimeException err = new RuntimeException(first);
err.addSuppressed(second);
throw err;
}
}
}
private static Class<?> getPortalRendererClass()
{
try
{
return Class.forName("qouteall.imm_ptl.core.render.renderer.PortalRenderer"); // 1.21+
}
catch (ClassNotFoundException first)
{
try
{
return Class.forName("qouteall.imm_ptl.core.render.PortalRenderer");
}
catch (ClassNotFoundException second)
{
try
{
return Class.forName("com.qouteall.immersive_portals.render.PortalRenderer"); // 1.16
}
catch (ClassNotFoundException third)
{
RuntimeException err = new RuntimeException(first);
err.addSuppressed(second);
err.addSuppressed(third);
throw err;
}
}
}
}
private static Class<?> getPortalClass()
{
try
{
portalClass = Class.forName("qouteall.imm_ptl.core.portal.Portal");
}
catch (ClassNotFoundException first)
{
try
{
portalClass = Class.forName("com.qouteall.immersive_portals.portal.Portal"); // 1.16
}
catch (ClassNotFoundException second)
{
RuntimeException err = new RuntimeException(first);
err.addSuppressed(second);
throw err;
}
}
return portalClass;
}
private static Class<?> getGlobalPortalStorageClass()
{
try
{
return Class.forName("qouteall.imm_ptl.core.portal.global_portals.GlobalPortalStorage");
}
catch (ClassNotFoundException first)
{
try
{
return Class.forName("com.qouteall.immersive_portals.McHelper"); // 1.16
}
catch (ClassNotFoundException second)
{
RuntimeException err = new RuntimeException(first);
err.addSuppressed(second);
throw err;
}
}
}
private static Class<?> getIPCGlobalClass()
{
try
{
return Class.forName("qouteall.imm_ptl.core.IPCGlobal");
}
catch (ClassNotFoundException first)
{
try
{
return Class.forName("com.qouteall.immersive_portals.CGlobal"); // 1.16
}
catch (ClassNotFoundException second)
{
RuntimeException err = new RuntimeException(first);
err.addSuppressed(second);
throw err;
}
}
}
private static boolean shouldSkipRenderingPortal(Object portal, Supplier<?> frustumSupplier)
{
try
{
if (shouldSkipRenderingPortalMethod == null)
{
shouldSkipRenderingPortalMethod = getPortalRendererClass().getDeclaredMethod(
"shouldSkipRenderingPortal", getPortalClass(), Supplier.class
);
shouldSkipRenderingPortalMethod.setAccessible(true);
}
if (Modifier.isStatic(shouldSkipRenderingPortalMethod.getModifiers()))
{
return (boolean) shouldSkipRenderingPortalMethod.invoke(null, portal, frustumSupplier);
}
else
{
return (boolean) shouldSkipRenderingPortalMethod.invoke(
getIPCGlobalClass().getField("renderer").get(null), portal, frustumSupplier
);
}
}
catch (Throwable e)
{
throw new RuntimeException(e);
}
}
protected abstract Object getClientLevel();
protected abstract Class<?> getLevelClass();
protected abstract Iterable<?> getEntitiesForRendering();
protected abstract Supplier<?> getFrustumSupplier();
private List<?> getGlobalPortals(Object level)
{
try
{
if (getGlobalPortalsMethodHandle == null)
{
getGlobalPortalsMethodHandle = MethodHandles.lookup().findStatic(
getGlobalPortalStorageClass(),
"getGlobalPortals", MethodType.methodType(List.class).appendParameterTypes(
getLevelClass()
)
);
}
return (List<?>) getGlobalPortalsMethodHandle.invoke(level);
}
catch (Throwable e)
{
throw new RuntimeException(e);
}
}
//endregion
//===========//
// overrides //
//===========//
//region
@Override
public boolean isRenderingPortal()
{
try
{
if (isRenderingMethodHandle == null)
{
isRenderingMethodHandle = MethodHandles.lookup().findStatic(
getPortalRenderingClass(),
"isRendering", MethodType.methodType(Boolean.TYPE)
);
}
return (boolean) isRenderingMethodHandle.invoke();
}
catch (Throwable e)
{
throw new RuntimeException(e);
}
}
@Override
public boolean wasPortalRecentlyVisible()
{
// I did consider setting portalVisible to true whenever PortalRendering::isRendering was true instead,
// but that would still render fading immediately after startup before entering the portal at least once.
// This is more robust, but slightly worse for performance. Still, people can just turn fading off if they have issues.
boolean isPortalVisible = isPortalVisibleRightNow();
if (isPortalVisible)
{
lastPortalMsTime = System.currentTimeMillis();
portalVisible = true;
}
else if (portalVisible)
{
if (System.currentTimeMillis() - lastPortalMsTime > 1000)
{
portalVisible = false;
}
}
// Simply checking portal visibility right now is not sufficient, that will still render the fading on top of the portal.
// Instead, we check if a portal was rendered during the last second or so.
return portalVisible;
}
/** Essentially reimplements PortalRenderer::getPortalsToRender because it did not exist in 1.16. */
private boolean isPortalVisibleRightNow()
{
Supplier<?> frustumSupplier = getFrustumSupplier();
if (frustumSupplier == null)
{
return false;
}
for (Object portal : getGlobalPortals(getClientLevel()))
{
if (!shouldSkipRenderingPortal(portal, frustumSupplier))
{
return true;
}
}
for (Object entity : getEntitiesForRendering())
{
if (isPortalObject(entity)
&& !shouldSkipRenderingPortal(entity, frustumSupplier))
{
return true;
}
}
return false;
}
private static boolean isPortalObject(Object object) { return getPortalClass().isInstance(object); }
//endregion
@Override
public String getModName() { return "Immersive Portals"; }
//=======//
// event //
//=======//
//region
private static class BeforeRenderEvent extends DhApiBeforeRenderEvent
{
@NotNull
private final IImmersivePortalsAccessor immersivePortals;
public BeforeRenderEvent(@NotNull IImmersivePortalsAccessor portalAccessor) { this.immersivePortals = portalAccessor; }
@Override
public void beforeRender(DhApiCancelableEventParam<DhApiRenderParam> event)
{
// needed because otherwise DH doesn't render to the level anyway
// and will probably render the level the player is currently in instead
if (this.immersivePortals.isRenderingPortal())
{
event.cancelEvent();
}
}
}
//endregion
}
@@ -23,10 +23,14 @@ import com.seibel.distanthorizons.api.interfaces.render.IDhApiCustomRenderRegist
import com.seibel.distanthorizons.core.render.RenderParams;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IProfilerWrapper;
public interface IDhGenericRenderer extends IDhApiCustomRenderRegister
public interface IDhGenericRenderer extends IDhApiCustomRenderRegister, AutoCloseable
{
void render(RenderParams renderEventParam, IProfilerWrapper profiler, boolean renderingWithSsao);
String getVboRenderDebugMenuString();
@Override void close(); // override to remove "throws exception"
}
@@ -20,6 +20,7 @@
package com.seibel.distanthorizons.core.wrapperInterfaces.world;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import org.jetbrains.annotations.Nullable;
@@ -29,6 +30,9 @@ import java.awt.*;
public interface IClientLevelWrapper extends ILevelWrapper
{
/** used to track when this level was last used for Immersive portals support */
void markAccessed();
@Nullable
IServerLevelWrapper tryGetServerSideWrapper();
@@ -39,4 +43,6 @@ public interface IClientLevelWrapper extends ILevelWrapper
Color getCloudColor(float tickDelta);
float getShade(EDhDirection lodDirection);
}
@@ -28,7 +28,10 @@
"API LOCK",
"distanthorizons.general.disabledByApi.@tooltip":
"This option is controlled by another mod via DH's API \nso it cannot be changed via the UI or config file.",
"distanthorizons.general.unsupportedMcVersion":
"VER LOCK",
"distanthorizons.general.unsupportedMcVersion.@tooltip":
"DH doesn't support changing this option on this version of Minecraft. \nAny config file or API set values will be ignored.",
@@ -188,11 +191,11 @@
"If true non terrain objects will be rendered in DH's terrain. \nThis includes beacon beams and clouds.",
"distanthorizons.config.client.advanced.graphics.genericRendering.enableBeaconRendering":
"Enable Beacon Rendering",
"distanthorizons.config.client.advanced.graphics.genericRendering.beaconRenderHeight":
"distanthorizons.config.client.advanced.graphics.genericRendering.beaconRenderHeight":
"Beacon render height",
"distanthorizons.config.client.advanced.graphics.genericRendering.beaconRenderHeight.@tooltip":
"Sets the maximum height at which beacons will render. \nThis will only affect new beacons coming into LOD render distance. \nBeacons currently visible in LOD chunks will not be affected.",
"distanthorizons.config.client.advanced.graphics.genericRendering.expandDistantBeacons":
"distanthorizons.config.client.advanced.graphics.genericRendering.expandDistantBeacons":
"Expand Distant Beacons",
"distanthorizons.config.client.advanced.graphics.genericRendering.expandDistantBeacons.@tooltip":
"If true LOD beacon beams will be rendered wider at extreme distances, \nmaking them easier to see. \nIf false all LOD beacon beams will only ever be 1 block wide.",
@@ -533,6 +536,26 @@
"distanthorizons.config.client.advanced.debugging.positionFinderDebugging":
"Position Finder",
"distanthorizons.config.client.advanced.debugging.positionFinderDebugging.positionFinderEnable":
"Enable Position Finder",
"distanthorizons.config.client.advanced.debugging.positionFinderDebugging.positionFinderDetailLevel":
"Absolute Detail Level",
"distanthorizons.config.client.advanced.debugging.positionFinderDebugging.positionFinderXPos":
"Pos X",
"distanthorizons.config.client.advanced.debugging.positionFinderDebugging.positionFinderZPos":
"Pos Z",
"distanthorizons.config.client.advanced.debugging.positionFinderDebugging.positionFinderMinBlockY":
"Min Block Y",
"distanthorizons.config.client.advanced.debugging.positionFinderDebugging.positionFinderMaxBlockY":
"Max Block Y",
"distanthorizons.config.client.advanced.debugging.positionFinderDebugging.positionFinderMarginPercent":
"Margin %",
"distanthorizons.config.client.advanced.debugging.f3Screen":
"F3 Screen",
@@ -39,7 +39,13 @@ import org.junit.Test;
public class PooledDataSourceCheckoutTest
{
@Test
/**
* commented out for now since it has
* a chance of breaking if any other tests
* using the same pools run at the same time
* or before this one.
*/
//@Test
public void TestCheckouts()
{
// something like this should probably be called before starting the test to ensure
+33 -27
View File
@@ -57,7 +57,7 @@ public class QuadTreeTest
public void BasicPositiveQuadTreeTest()
{
AbstractTestTreeParams treeParams = new LargeTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL);
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL);
Assert.assertTrue("Tree min/max detail level out of expected bounds: " + tree, tree.treeRootDetailLevel >= 10 && tree.treeLeafDetailLevel <= 10 - 4);
@@ -93,7 +93,7 @@ public class QuadTreeTest
public void BasicNegativeQuadTreeTest()
{
AbstractTestTreeParams treeParams = new LargeTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), DhBlockPos2D.ZERO, LodUtil.BLOCK_DETAIL_LEVEL);
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), DhBlockPos2D.ZERO, LodUtil.BLOCK_DETAIL_LEVEL);
// root node //
@@ -128,7 +128,7 @@ public class QuadTreeTest
public void OutOfBoundsQuadTreeTest()
{
AbstractTestTreeParams treeParams = new LargeTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), new DhBlockPos2D(0, 0), LodUtil.BLOCK_DETAIL_LEVEL);
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), new DhBlockPos2D(0, 0), LodUtil.BLOCK_DETAIL_LEVEL);
Assert.assertEquals("tree diameter incorrect", treeParams.getWidthInBlocks(), tree.diameterInBlocks());
@@ -170,7 +170,7 @@ public class QuadTreeTest
public void outOfBoundsInTreeTest()
{
// very specific tree parameters to match test results
QuadTree<Integer> tree = new QuadTree<>(512, new DhBlockPos2D(125, -516), (byte) 6);
QuadTree<Integer> tree = new QuadTree<>(512, 8, new DhBlockPos2D(125, -516), (byte) 6);
Assert.assertEquals("Test may need to be re-calculated for different max detail level.", 9, tree.treeRootDetailLevel);
@@ -192,7 +192,7 @@ public class QuadTreeTest
public void QuadTreeRootAlignedMovingTest()
{
AbstractTestTreeParams treeParams = new LargeTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL);
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL);
int pseudoRootNodeWidthInBlocks = BitShiftUtil.powerOfTwo(10);
@@ -210,7 +210,7 @@ public class QuadTreeTest
testSet(tree, ne, 3);
testSet(tree, sw, 4);
testSet(tree, se, 5);
Assert.assertEquals("incorrect leaf node count", tree.leafNodeCount(), 4);
Assert.assertEquals("incorrect leaf node count", 4, tree.leafNodeCount());
// fake move //
@@ -237,7 +237,7 @@ public class QuadTreeTest
testGet(tree, ne, 3);
testGet(tree, sw, 4);
testGet(tree, se, 5);
Assert.assertEquals("incorrect leaf node count", tree.leafNodeCount(), 4);
Assert.assertEquals("incorrect leaf node count", 4, tree.leafNodeCount());
@@ -252,7 +252,7 @@ public class QuadTreeTest
testGet(tree, sw, null, IndexOutOfBoundsException.class);
testGet(tree, se, null, IndexOutOfBoundsException.class);
Assert.assertEquals("incorrect leaf node count", tree.leafNodeCount(), 0);
Assert.assertEquals("incorrect leaf node count", 0, tree.leafNodeCount());
@@ -276,7 +276,7 @@ public class QuadTreeTest
DhBlockPos2D edgeMoveBlockPos = new DhBlockPos2D(pseudoRootNodeWidthInBlocks - (treeParams.getWidthInRootNodes() * pseudoRootNodeWidthInBlocks), 0);
tree.setCenterBlockPos(edgeMoveBlockPos);
Assert.assertEquals("Tree center incorrect", edgeMoveBlockPos, tree.getCenterBlockPos());
Assert.assertEquals("incorrect leaf node count", 2, tree.leafNodeCount());
Assert.assertEquals("incorrect leaf node count", 0, tree.leafNodeCount());
}
@@ -284,7 +284,7 @@ public class QuadTreeTest
public void QuadTreeIterationTest()
{
AbstractTestTreeParams treeParams = new LargeTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL);
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL);
// (pseudo) root nodes //
@@ -336,7 +336,7 @@ public class QuadTreeTest
public void QuadTreeIterationFilterTest()
{
AbstractTestTreeParams treeParams = new TinyTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getPositiveEdgeCenterPos(), (byte)0);
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), treeParams.getPositiveEdgeCenterPos(), (byte)0);
@@ -418,7 +418,10 @@ public class QuadTreeTest
}
private static <T> void assertFilterCount(QuadTree<T> tree, String message, int expectedNodeCount, @Nullable QuadTree.INodeIteratorStoppingFunc<T> stoppingFilterFunc)
private static <T> void assertFilterCount(
QuadTree<T> tree, String message,
int expectedNodeCount,
@Nullable QuadTree.INodeIteratorStoppingFunc<T> stoppingFilterFunc)
{
ArrayList<String> foundNodePositionStrings = new ArrayList<>();
@@ -532,7 +535,7 @@ public class QuadTreeTest
public void CenteredGridListIterationTest()
{
AbstractTestTreeParams treeParams = new TinyTestTree();
final QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL);
final QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL);
testSet(tree, DhSectionPos.encode(tree.treeRootDetailLevel, 0, 0), 0);
// confirm the root node were added
@@ -573,18 +576,18 @@ public class QuadTreeTest
AbstractTestTreeParams treeParams = new TinyTestTree();
// exactly inside (5*0,0)
testGridListRootCount(treeParams.getWidthInBlocks(), treeParams.getPositiveEdgeCenterPos(), 1);
testGridListRootCount(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), treeParams.getPositiveEdgeCenterPos(), 1);
// offset across (5*-1,0) and (5*0,0)
testGridListRootCount(treeParams.getWidthInBlocks(), new DhBlockPos2D(-treeParams.getWidthInBlocks() / 4, treeParams.getPositiveEdgeCenterPos().z), 2);
testGridListRootCount(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), new DhBlockPos2D(-treeParams.getWidthInBlocks() / 4, treeParams.getPositiveEdgeCenterPos().z), 2);
// offset across the origin: (5*0,0), (5*-1,0), (5*0,-1), and (5*-1,-1)
testGridListRootCount(treeParams.getWidthInBlocks(), DhBlockPos2D.ZERO, 4);
testGridListRootCount(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), DhBlockPos2D.ZERO, 4);
}
private static void testGridListRootCount(int treeWidth, DhBlockPos2D treeMovePos, int expectedRootNodeCount)
private static void testGridListRootCount(int treeWidth, int treeDistanceForNodeClearing, DhBlockPos2D treeMovePos, int expectedRootNodeCount)
{
final QuadTree<Integer> tree = new QuadTree<>(treeWidth, DhBlockPos2D.ZERO, LodUtil.BLOCK_DETAIL_LEVEL);
final QuadTree<Integer> tree = new QuadTree<>(treeWidth, treeDistanceForNodeClearing, DhBlockPos2D.ZERO, LodUtil.BLOCK_DETAIL_LEVEL);
Assert.assertEquals("tree creation failed, incorrect initial position", DhBlockPos2D.ZERO, tree.getCenterBlockPos());
tree.setCenterBlockPos(treeMovePos);
@@ -615,7 +618,7 @@ public class QuadTreeTest
public void TinyGridAlignedTreeTest()
{
AbstractTestTreeParams treeParams = new MediumTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL);
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), treeParams.getPositiveEdgeCenterPos(), LodUtil.BLOCK_DETAIL_LEVEL);
// minimum size tree should be 3 root nodes wide
Assert.assertEquals("incorrect tree node width", 3, tree.ringListWidth());
Assert.assertEquals("incorrect tree width", treeParams.getWidthInBlocks(), tree.diameterInBlocks());
@@ -644,7 +647,7 @@ public class QuadTreeTest
public void TinyGridOffsetTreeTest()
{
AbstractTestTreeParams treeParams = new MediumTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), new DhBlockPos2D(0, 0), LodUtil.BLOCK_DETAIL_LEVEL);
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), new DhBlockPos2D(0, 0), LodUtil.BLOCK_DETAIL_LEVEL);
// minimum size tree should be 3 root nodes wide
Assert.assertEquals("incorrect tree node width", 3, tree.ringListWidth());
Assert.assertEquals("incorrect tree width", treeParams.getWidthInBlocks(), tree.diameterInBlocks());
@@ -683,7 +686,7 @@ public class QuadTreeTest
public void TreeDetailLevelLimitTest()
{
AbstractTestTreeParams treeParams = new MediumTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), new DhBlockPos2D(0, 0), (byte) 8);
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), new DhBlockPos2D(0, 0), (byte) 8);
Assert.assertEquals("Test detail level's need to be adjusted. This isn't necessarily a failed test.", 10, tree.treeRootDetailLevel);
// valid detail levels
@@ -705,7 +708,7 @@ public class QuadTreeTest
public void QuadNodeDetailLimitTest()
{
AbstractTestTreeParams treeParams = new MediumTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), new DhBlockPos2D(0, 0), (byte) 6);
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), new DhBlockPos2D(0, 0), (byte) 6);
Assert.assertEquals("Test detail level's need to be adjusted. This isn't necessarily a failed test.", 10, tree.treeRootDetailLevel);
// create the root node
@@ -817,8 +820,7 @@ public class QuadTreeTest
@Test
public void quadNodeChildPositionOutOfBoundsTest()
{
int treeWidthInBlocks = 64;
QuadTree<Integer> tree = new QuadTree<>(treeWidthInBlocks, new DhBlockPos2D(-2, 0), (byte) 0);
QuadTree<Integer> tree = new QuadTree<>(64, 1, new DhBlockPos2D(-2, 0), (byte) 0);
@@ -865,7 +867,7 @@ public class QuadTreeTest
public void toStringTest()
{
AbstractTestTreeParams treeParams = new MediumTestTree();
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), new DhBlockPos2D(0, 0), (byte) 6);
QuadTree<Integer> tree = new QuadTree<>(treeParams.getWidthInBlocks(), treeParams.getBlockDistanceForNodeClearing(), new DhBlockPos2D(0, 0), (byte) 6);
String treeString = tree.toString();
Assert.assertNotNull(treeString);
@@ -929,25 +931,28 @@ public class QuadTreeTest
private abstract static class AbstractTestTreeParams
{
public abstract int getWidthInBlocks();
public abstract int getBlockDistanceForNodeClearing();
/** the tree should be slightly larger than the width in blocks to account for offset centers */
public int getWidthInRootNodes() { return MathUtil.log2(this.getWidthInBlocks()) + 2; }
/** the top (root) detail level in the tree */
public byte getMinDetailLevel() { return (byte) MathUtil.log2(this.getWidthInBlocks()); }
public byte getRootDetailLevel() { return (byte) MathUtil.log2(this.getWidthInBlocks()); }
/** @return the block pos so that the tree's negative corner lines up with (0,0) */
public DhBlockPos2D getPositiveEdgeCenterPos() { return new DhBlockPos2D(BitShiftUtil.powerOfTwo(this.getMinDetailLevel()) / 2, BitShiftUtil.powerOfTwo(this.getMinDetailLevel()) / 2); }
public DhBlockPos2D getPositiveEdgeCenterPos() { return new DhBlockPos2D(BitShiftUtil.powerOfTwo(this.getRootDetailLevel()) / 2, BitShiftUtil.powerOfTwo(this.getRootDetailLevel()) / 2); }
}
private static class LargeTestTree extends AbstractTestTreeParams
{
public int getWidthInBlocks() { return 8192; }
public int getBlockDistanceForNodeClearing() { return 16; }
}
private static class MediumTestTree extends AbstractTestTreeParams
{
public int getWidthInBlocks() { return 1024; }
public int getBlockDistanceForNodeClearing() { return 4; }
}
@@ -955,6 +960,7 @@ public class QuadTreeTest
{
// top detail level = 6
public int getWidthInBlocks() { return 32; }
public int getBlockDistanceForNodeClearing() { return 1; }
}