Compare commits

...

71 Commits

Author SHA1 Message Date
James Seibel 269f2c30fd Initial changes for Vulkan 2026-05-11 21:56:53 -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
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 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
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
James Seibel 22efbb211a remove dev from version number 2026-04-18 21:43:37 -05:00
James Seibel 95c4459d8a compile separate combined API jar 2026-04-18 15:47:23 -05:00
James Seibel 0ef11caaf2 API jar auto include sources 2026-04-18 12:07:24 -05:00
James Seibel 2e3dfab6c3 fix api javadoc compiling 2026-04-18 12:06:14 -05:00
James Seibel 42be139e94 remove google-collect 2026-04-18 11:07:12 -05:00
James Seibel f866e7f8e3 up version number 3.0.0 -> 3.0.1 2026-04-18 10:36:52 -05:00
James Seibel 53fcce9d7c remove dev from version number 2026-04-18 10:34:57 -05:00
James Seibel 7b8b22fd5a terrain data cash override close without exception 2026-04-15 07:46:49 -05:00
James Seibel 6f54cfacb5 fix unit test compiling 2026-04-14 20:38:05 -05:00
James Seibel 61eaf43ba0 Add DhApiBlockColorOverrideEvent 2026-04-14 20:35:38 -05:00
James Seibel 1d368e3adc Add DhApiBlockStateWrapperCreatedEvent 2026-04-14 19:43:37 -05:00
James Seibel 9e65e2dd4c comment/deprecate a few API events 2026-04-14 19:03:00 -05:00
James Seibel 2d878338cb Merge branch 'change/channel_name_compat' 2026-04-14 17:08:12 -05:00
James Seibel d62d21776d fix IBO buffer creation size 2026-04-12 15:17:48 -05:00
James Seibel a44c5d562d Move native dialog to common
Native dialog was changed with LWJGL 3.4.1
2026-04-12 13:53:31 -05:00
James Seibel 1d9bffe64e only run world gen for rendering levels
Also fix world gen progress getting stuck on a single level
2026-04-11 21:40:28 -05:00
James Seibel b4e0687e2a fix gitlab auto updater failing for MC 26 2026-04-11 16:58:23 -05:00
James Seibel 89804f1ba1 add documentation about nullable IDhApiUnsafeWrapper 2026-04-11 15:23:00 -05:00
James Seibel 9dbc5ef525 fix white beacons colored incorrectly 2026-04-11 12:34:10 -05:00
James Seibel e5dcb0999d minor optifine optimization 2026-04-11 11:11:48 -05:00
James Seibel 50e0e940d1 profile wrapper try-finally for pushes 2026-04-11 11:03:56 -05:00
James Seibel cb3e42fac4 Merge branch 'client-updates' into 'main'
Don't drop client updates if level is not loaded yet

See merge request distant-horizons-team/distant-horizons-core!98
2026-04-09 19:44:12 +00:00
Fabian Maurer 34cdaf02eb Don't drop client updates if level is not loaded yet 2026-04-08 22:53:58 +02:00
Jim C K Flaten b0e7c31964 Comment 2026-01-05 21:32:41 +01:00
Jim C K Flaten 2e906b57c4 Channel name should be 20 chars or shorter for compatibility with old version of Minecraft. 2026-01-03 23:36:46 +01:00
76 changed files with 2460 additions and 1373 deletions
+39 -19
View File
@@ -23,43 +23,42 @@ dependencies {
testImplementation "junit:junit:4.13"
}
shadowJar {
// required for basic shadowJar setup
configurations = [project.configurations.shadow]
java {
withSourcesJar()
}
task addSourcesToCompiledJar(type: ShadowJar) {
task createReleaseApiJar(type: ShadowJar) {
def sourceJarPath = "build/libs/DistantHorizons-api-${rootProject.versionStr}-sources.jar"
def secondJarFile = file(sourceJarPath)
mustRunAfter sourcesJar
dependsOn shadowJar
// the compiled "-all" jar is used since it's available at the time this task is run
def compiledJarPath = "build/libs/DistantHorizonsApi-${rootProject.api_version}-all.jar"
def compiledJarFile = file(compiledJarPath)
// doFirst is so these only run when the task is actually executed
doFirst {
System.out.println("Adding source files from: \n" +
"[" + sourceJarPath + "] to compiled API jar: \n" +
System.out.println("Adding class files from: \n" +
"[" + compiledJarPath + "] to source API jar: \n" +
"[" + shadowJar.archiveFile.get().asFile + "]")
// Validate the input JAR file
if (!secondJarFile.exists()) {
throw new GradleException("Second JAR file not found: [${secondJarFile}]")
if (!compiledJarFile.exists()) {
throw new GradleException("Compiled JAR file not found: [${compiledJarFile}]")
}
}
// Set the name of the combined JAR file
archiveFileName.set("DistantHorizonsApi-${rootProject.api_version}.jar")
// Set the destination directory for the combined JAR file
destinationDirectory = file('build/libs/merged/')
archiveFileName.set("DistantHorizonsApi-${rootProject.api_version}-combined.jar") // jar name
destinationDirectory = file('build/libs/') // jar location
// Set the input JAR files to be combined
from sourceSets.main.allJava
from {
configurations.shadow.collect { it.isDirectory() ? it : zipTree(it) }
project.configurations.shadow.collect { it.isDirectory() ? it : zipTree(it) }
}
// set the jars to merge
from shadowJar.archiveFile.get().asFile
from secondJarFile
// add the class files
from zipTree(compiledJarFile)
// alternative method to Include the source files in the combined JAR
// and/or see which files are being included
@@ -93,6 +92,13 @@ task addSourcesToCompiledJar(type: ShadowJar) {
}
}
// always create a combined jar for easy deployment
assemble.dependsOn(createReleaseApiJar)
shadowJar {
// required for basic shadowJar setup
configurations = [project.configurations.shadow]
}
javadoc {
options {
@@ -103,3 +109,17 @@ javadoc {
addStringOption('Xdoclint:all,-missing', '-quiet')
}
}
// set the jar name
def configureJar = { task ->
// outputs in the format:
// "DistantHorizonsApi-6.0.0.jar"
// "DistantHorizonsApi-6.0.0-sources.jar"
task.archiveBaseName = rootProject.api_name
task.archiveVersion = rootProject.api_version
}
configureJar(tasks.named("jar").get())
configureJar(tasks.named("sourcesJar").get())
configureJar(tasks.named("shadowJar").get())
@@ -19,6 +19,8 @@
package com.seibel.distanthorizons.api.interfaces;
import com.seibel.distanthorizons.api.interfaces.block.IDhApiBlockStateWrapper;
/**
* Implemented by wrappers so developers can
* access the underlying Minecraft object(s).
@@ -38,7 +40,11 @@ public interface IDhApiUnsafeWrapper
* In order to cast this object to something usable, you may want
* to use <code>obj.getClass()</code> when in your IDE
* in order to determine what object this method returns for
* the specific version of Minecraft you are developing for.
* the specific version of Minecraft you are developing for. <br><br>
*
* Note:<br>
* This method may return null in some cases, IE {@link IDhApiBlockStateWrapper}
* when it is wrapping air.
*/
Object getWrappedMcObject();
@@ -23,7 +23,10 @@ import com.seibel.distanthorizons.api.enums.rendering.EDhApiBlockMaterial;
import com.seibel.distanthorizons.api.interfaces.IDhApiUnsafeWrapper;
/**
* A Minecraft version independent way of handling Blocks.
* A Minecraft version independent way of handling Blocks. <br><br>
*
* Note: the wrapped object (IE the object returned by {@link IDhApiUnsafeWrapper#getWrappedMcObject})
* will be null if this object is wrapping air.
*
* @author James Seibel
* @version 2023-6-11
@@ -39,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.
@@ -24,4 +24,10 @@ public interface IDhApiTerrainDataCache extends AutoCloseable
*/
void clear();
// override without an exception
@Override
void close();
}
@@ -51,14 +51,16 @@ public interface IDhApiRenderProxy
//=======================//
/**
* Returns the name of Distant Horizons' depth texture. <br>
* Will return {@link DhApiResult#success} = false and {@link DhApiResult#payload} = -1 if the texture hasn't been created yet.
* Returns the OpenGL name of Distant Horizons' depth texture. <br>
* Will return {@link DhApiResult#success} = false and {@link DhApiResult#payload} = -1 if the texture hasn't been created yet
* or a rendering API other than OpenGL is in use.
*/
DhApiResult<Integer> getDhDepthTextureId();
/**
* Returns the name of Distant Horizons' color texture. <br>
* Will return {@link DhApiResult#success} = false and {@link DhApiResult#payload} = -1 if the texture hasn't been created yet.
* Returns the OpenGL name of Distant Horizons' color texture. <br>
* Will return {@link DhApiResult#success} = false and {@link DhApiResult#payload} = -1 if the texture hasn't been created yet
* or a rendering API other than OpenGL is in use
*/
DhApiResult<Integer> getDhColorTextureId();
@@ -27,7 +27,7 @@ import com.seibel.distanthorizons.api.objects.math.DhApiVec3f;
/**
* Called before Distant Horizons starts rendering a buffer. <br>
* This event cannot be cancelled, use {@link DhApiBeforeRenderEvent} if you want to cancel rendering.
* This event cannot be canceled, use {@link DhApiBeforeRenderEvent} if you want to cancel rendering.
*
* @author James Seibel
* @version 2023-1-31
@@ -31,7 +31,10 @@ import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhAp
* @author James Seibel
* @version 2025-6-9
* @since API 4.1.0
* @deprecated Only used for the legacy OpenGL renderer <Br>
* Using {@link DhApiAfterColorDepthTextureCreatedEvent} instead is recommended.
*/
@Deprecated
public abstract class DhApiBeforeColorDepthTextureCreatedEvent implements IDhApiEvent<DhApiTextureCreatedParam>
{
/** Fired before Distant Horizons creates. */
@@ -0,0 +1,156 @@
/*
* 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.api.methods.events.abstractEvents;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiBlockMaterial;
import com.seibel.distanthorizons.api.interfaces.block.IDhApiBlockStateWrapper;
import com.seibel.distanthorizons.api.interfaces.world.IDhApiLevelWrapper;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEvent;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiEventParam;
import com.seibel.distanthorizons.coreapi.util.ColorUtil;
import java.awt.*;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Performance note: this event will be fired thousands of times on concurrent threads,
* make it thread safe and as fast as possible. <Br><Br>
*
* This event is fired when DH needs to convert a {@link IDhApiBlockStateWrapper}
* into a color for rendering. This event is fired after DH attempts to determine
* the color itself (using the base color, tinting, etc.). <Br>
* Using this event will override the tinting config for this block
* unless you re-implement that logic yourself.
* <Br><Br>
*
* This event will only trigger for {@link IDhApiBlockStateWrapper}s that have been registered
* via {@link DhApiBlockStateWrapperCreatedEvent.EventParam#setAllowApiColorOverride(boolean)}.
*
* @author James Seibel
* @version 2026-04-14
* @since API 6.0.0
* @see IDhApiBlockStateWrapper
*/
public abstract class DhApiBlockColorOverrideEvent implements IDhApiEvent<DhApiBlockColorOverrideEvent.EventParam>
{
public abstract void blockStateWrapperCreated(DhApiEventParam<EventParam> event);
//=========================//
// internal DH API methods //
//=========================//
@Override
public final void fireEvent(DhApiEventParam<EventParam> event) { this.blockStateWrapperCreated(event); }
//==================//
// parameter object //
//==================//
public static class EventParam implements IDhApiEventParam
{
private IDhApiLevelWrapper levelWrapper;
private IDhApiBlockStateWrapper blockStateWrapper = null;
private int colorAsInt = -1;
private int blockPosX = 0, blockPosY = 0, blockPosZ = 0;
//=============//
// constructor //
//=============//
public EventParam() {}
public void update(
IDhApiLevelWrapper levelWrapper,
IDhApiBlockStateWrapper blockStateWrapper,
int colorAsInt,
int blockPosX, int blockPosY, int blockPosZ)
{
this.levelWrapper = levelWrapper;
this.blockStateWrapper = blockStateWrapper;
this.colorAsInt = colorAsInt;
this.blockPosX = blockPosX;
this.blockPosY = blockPosY;
this.blockPosZ = blockPosZ;
}
//=================//
// getters/setters //
//=================//
public IDhApiBlockStateWrapper getBlockStateWrapper() { return this.blockStateWrapper; }
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 { 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.argbToInt(alpha, red, green, blue);
}
/** @return the block's X value in the world */
public int getBlockPosX() { return blockPosX; }
/** @return the block's Y value in the world */
public int getBlockPosY() { return blockPosY; }
/** @return the block's Z value in the world */
public int getBlockPosZ() { return blockPosZ; }
/**
* Returns the same instance of this event.
* Copying this event isn't supported
* since the internal parameters must be mutated
* by API users in order to be tracked by DH's internal
* logic.
*/
@Override
public EventParam copy() { return this; }
@Override
public boolean getCopyBeforeFire() { return false; }
}
}
@@ -0,0 +1,131 @@
/*
* 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.api.methods.events.abstractEvents;
import com.seibel.distanthorizons.api.enums.rendering.EDhApiBlockMaterial;
import com.seibel.distanthorizons.api.interfaces.block.IDhApiBlockStateWrapper;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEvent;
import com.seibel.distanthorizons.api.methods.events.interfaces.IDhApiEventParam;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiEventParam;
/**
* Can be used to modify {@link IDhApiBlockStateWrapper}'s as they're created.
* This can be helpful for modded blocks that are mis-categorized by DH's base logic. <br><br>
*
* Note: this is only fired once per {@link IDhApiBlockStateWrapper} that is created
* and those {@link IDhApiBlockStateWrapper} will only be created once per JVM session.
*
* @author James Seibel
* @version 2026-04-14
* @since API 6.0.0
* @see IDhApiBlockStateWrapper
*/
public abstract class DhApiBlockStateWrapperCreatedEvent implements IDhApiEvent<DhApiBlockStateWrapperCreatedEvent.EventParam>
{
public abstract void blockStateWrapperCreated(DhApiEventParam<EventParam> event);
//=========================//
// internal DH API methods //
//=========================//
@Override
public final void fireEvent(DhApiEventParam<EventParam> event) { this.blockStateWrapperCreated(event); }
//==================//
// parameter object //
//==================//
public static class EventParam implements IDhApiEventParam
{
/**
* A copy of the wrapper that will be created. <Br>
* Note: modifying this object won't change anything
* a new wrapper will be created after this event finishes.
*/
private final IDhApiBlockStateWrapper blockStateWrapper;
private boolean overridesSet = false;
private EDhApiBlockMaterial blockMaterial = null;
private Integer opacity = null;
private Boolean allowApiColorOverride = null;
//=============//
// constructor //
//=============//
public EventParam(IDhApiBlockStateWrapper blockStateWrapper) { this.blockStateWrapper = blockStateWrapper; }
//=================//
// getters/setters //
//=================//
public IDhApiBlockStateWrapper getBlockStateWrapper() { return this.blockStateWrapper; }
/** if set this will override the value currently set in the given {@link IDhApiBlockStateWrapper} */
public void setBlockMaterial(EDhApiBlockMaterial blockMaterial)
{
this.blockMaterial = blockMaterial;
this.overridesSet = true;
}
public EDhApiBlockMaterial getBlockMaterial() { return this.blockMaterial; }
/** if set this will override the value currently set in the given {@link IDhApiBlockStateWrapper} */
public void setOpacity(int opacity)
{
this.opacity = opacity;
this.overridesSet = true;
}
public Integer getOpacity() { return this.opacity; }
/** if set to true this {@link IDhApiBlockStateWrapper} will trigger {@link DhApiBlockColorOverrideEvent} */
public void setAllowApiColorOverride(boolean allowApiColorOverride)
{
this.allowApiColorOverride = allowApiColorOverride;
this.overridesSet = true;
}
public Boolean getAllowApiColorOverride() { return this.allowApiColorOverride; }
/** If true then one or more options for this block were set to be changed */
public boolean getOverridesSet() { return this.overridesSet; }
/**
* Returns the same instance of this event.
* Copying this event isn't supported
* since the internal parameters must be mutated
* by API users in order to be tracked by DH's internal
* logic.
*/
@Override
public EventParam copy() { return this; }
@Override
public boolean getCopyBeforeFire() { return false; }
}
}
@@ -31,7 +31,9 @@ import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhAp
* @author James Seibel
* @version 2024-3-2
* @since API 2.0.0
* @deprecated Replaced by {@link DhApiBeforeColorDepthTextureCreatedEvent} since this event's name isn't obvious when it fires.
* @deprecated Only used for the legacy OpenGL renderer <Br>
* Replaced by {@link DhApiBeforeColorDepthTextureCreatedEvent} since this event's name isn't obvious when it fires.
* Using {@link DhApiAfterColorDepthTextureCreatedEvent} instead is recommended
*/
@Deprecated // internal notes: this method must be kept around due to Iris using it and we don't want to break old Iris support
public abstract class DhApiColorDepthTextureCreatedEvent implements IDhApiEvent<DhApiColorDepthTextureCreatedEvent.EventParam>
@@ -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;
@@ -32,22 +32,27 @@ public final class ModInfo
/** Incremented every time any packets are added, changed or removed, with a few exceptions. */
public static final int PROTOCOL_VERSION = 13;
public static final String WRAPPER_PACKET_PATH = "message";
/**
* The full plugin channel name (RESOURCE_NAMESPACE:WRAPPER_PACKET_PATH)
* must be 20 characters or fewer for compatibility with MC 1.13 and older.
*/
public static final String WRAPPER_PACKET_PATH = "msg";
/** The internal mod name */
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.0-b-dev";
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;
@@ -17,7 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.util;
package com.seibel.distanthorizons.coreapi.util;
import java.awt.*;
@@ -89,6 +89,16 @@ public class ColorUtil
/** @param newBlue should be a value between 0 and 255 */
public static int setBlue(int color, int newBlue) { return (getAlpha(color) << 24) | (getRed(color) << 16) | (getGreen(color) << 8) | newBlue; }
/** @throws IllegalArgumentException if the given int value is out of the range 0 - 255 (exclusive) */
public static void throwIfColorValueOutOfIntRange(String colorName, int value) throws IllegalArgumentException
{
if (value < 0
|| value > 255)
{
throw new IllegalArgumentException("["+colorName+"] with the value ["+value+"] is out of the expected range 0 - 255 (exclusive).");
}
}
public static int applyShade(int color, int shade)
-1
View File
@@ -50,7 +50,6 @@ dependencies {
compileOnly("io.netty:netty-buffer:${rootProject.netty_version}")
compileOnly("org.jetbrains:annotations:16.0.2")
compileOnly("com.google.code.findbugs:jsr305:3.0.2")
compileOnly("com.google.common:google-collect:0.5")
compileOnly("com.google.guava:guava:31.1-jre")
// DH's bundled libraries (shadowed + relocated in loader jars)
@@ -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,7 @@ 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.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;
@@ -63,6 +65,7 @@ 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.LinkedBlockingQueue;
@@ -145,6 +148,16 @@ public class ClientApi
private Vec3d lastCameraPosForSpeedCheck = new Vec3d();
private long msSinceLastSpeedCheck = 0L;
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;
@@ -159,7 +172,7 @@ public class ClientApi
//==============//
// world events //
//==============//
///region
//region world events
/**
* May be fired slightly before or after the associated
@@ -237,14 +250,14 @@ public class ClientApi
this.waitingClientLevels.clear();
}
///endregion
//endregion
//==============//
// level events //
//==============//
///region
//region level events
public void clientLevelUnloadEvent(IClientLevelWrapper level)
{
@@ -356,14 +369,14 @@ public class ClientApi
}
}
///endregion
//endregion
//============//
// networking //
//============//
///region
//region networking
/**
* Forwards a decoded message into the registered handlers.
@@ -396,14 +409,14 @@ public class ClientApi
}
}
///endregion
//endregion
//===============//
// LOD rendering //
//===============//
///region
//region lod rendering
/** Should be called before {@link ClientApi#renderDeferredLodsForShaders} */
public void renderLods() { this.renderLodLayer(false); }
@@ -414,12 +427,11 @@ public class ClientApi
*/
public void renderDeferredLodsForShaders() { this.renderLodLayer(true); }
public static long firstRenderTimeMs = 0;
private void renderLodLayer(boolean renderingDeferredLayer)
{
IProfilerWrapper profiler = MC_CLIENT.getProfiler();
profiler.push("DH-RenderLevel");
try (IProfilerWrapper.IProfileBlock dhRender_profile = profiler.push("DH-RenderLevel"))
{
@@ -428,12 +440,16 @@ public class ClientApi
//===========//
//region
// 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
@@ -442,15 +458,13 @@ public class ClientApi
//=====================//
// render thread tasks //
//=====================//
///region
//region
// only run these tasks once per frame
if (!renderingDeferredLayer)
{
profiler.push("DH render thread tasks");
try (IProfilerWrapper.IProfileBlock renderTask_profile = profiler.push("DH render thread tasks"))
{
//===============//
// chat messages //
//===============//
@@ -462,6 +476,7 @@ public class ClientApi
//======================//
// GL Proxy queued jobs //
//======================//
//region
try
{
@@ -473,11 +488,14 @@ public class ClientApi
LOGGER.error("Unexpected issue running render thread tasks, error: [" + e.getMessage() + "].", e);
}
//endregion
//==============//
// camera speed //
//==============//
//region
long nowMs = System.currentTimeMillis();
if (this.msSinceLastSpeedCheck + MIN_MS_BETWEEN_SPEED_CHECKS < nowMs)
@@ -496,11 +514,32 @@ public class ClientApi
this.lastCameraPosForSpeedCheck = camPos;
}
//endregion
profiler.pop();
//====================//
// 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
//endregion
}
}
//endregion
@@ -508,7 +547,7 @@ public class ClientApi
//=================//
// parameter setup //
//=================//
///region
//region
EDhApiRenderPass renderPass;
if (DhApiRenderProxy.INSTANCE.getDeferTransparentRendering())
@@ -533,14 +572,14 @@ public class ClientApi
// partially complete info, but there isn't a better option at the moment
RenderParams renderParams = new RenderParams(renderPass, RENDER_STATE);
///endregion
//endregion
//============//
// validation //
//============//
///region
//region
if (firstRenderTimeMs == 0)
{
@@ -577,14 +616,14 @@ public class ClientApi
return;
}
///endregion
//endregion
//===========//
// rendering //
//===========//
///region
//region
try
{
@@ -654,21 +693,18 @@ public class ClientApi
MC_CLIENT.sendChatMessage(MinecraftTextFormat.DARK_RED + "Error: " + MinecraftTextFormat.CLEAR_FORMATTING + e);
}
///endregion
profiler.pop(); // end LOD
//endregion
}
}
///endregion
//endregion
//================//
// fade rendering //
//================//
///region
//region fade rendering
/**
* The first fade pass.
@@ -731,14 +767,14 @@ public class ClientApi
}
}
///endregion
//endregion
//==========//
// keyboard //
//==========//
///region
//region keyboard
/** Trigger once on key press, with CLIENT PLAYER. */
public void keyPressedEvent(int glfwKey)
@@ -767,14 +803,14 @@ public class ClientApi
}
}
///endregion
//endregion
//======//
// chat //
//======//
///region
//region chat
private void sendQueuedChatMessages()
{
@@ -908,7 +944,7 @@ public class ClientApi
*/
public void showOverlayMessageNextFrame(String message) { this.overlayMessageQueueForNextFrame.add(message); }
///endregion
//endregion
@@ -122,7 +122,7 @@ public class ServerApi
public void serverPlayerJoinEvent(IServerPlayerWrapper player)
{
if (DhApiWorldProxy.INSTANCE.worldLoaded() && DhApiWorldProxy.INSTANCE.getReadOnly())
if (DhApiWorldProxy.INSTANCE.tryGetReadOnly())
{
return;
}
@@ -136,7 +136,7 @@ public class ServerApi
}
public void serverPlayerDisconnectEvent(IServerPlayerWrapper player)
{
if (DhApiWorldProxy.INSTANCE.worldLoaded() && DhApiWorldProxy.INSTANCE.getReadOnly())
if (DhApiWorldProxy.INSTANCE.tryGetReadOnly())
{
return;
}
@@ -150,7 +150,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 +170,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
@@ -205,14 +204,14 @@ public class SharedApi
// If the client world isn't loaded yet, keep track of which chunks were loaded so we can use them later.
// This may happen if the client world and client level load events happen out of order
IClientLevelWrapper clientLevel = (IClientLevelWrapper) levelWrapper;
ClientApi.INSTANCE.waitingChunkByClientLevelAndPos.replace(new Pair<>(clientLevel, chunkWrapper.getChunkPos()), chunkWrapper);
ClientApi.INSTANCE.waitingChunkByClientLevelAndPos.put(new Pair<>(clientLevel, chunkWrapper.getChunkPos()), chunkWrapper);
}
return;
}
// ignore updates if the world is read-only
if (DhApiWorldProxy.INSTANCE.getReadOnly())
if (DhApiWorldProxy.INSTANCE.tryGetReadOnly())
{
return;
}
@@ -225,7 +224,7 @@ public class SharedApi
{
// the client level isn't loaded yet
IClientLevelWrapper clientLevel = (IClientLevelWrapper) levelWrapper;
ClientApi.INSTANCE.waitingChunkByClientLevelAndPos.replace(new Pair<>(clientLevel, chunkWrapper.getChunkPos()), chunkWrapper);
ClientApi.INSTANCE.waitingChunkByClientLevelAndPos.put(new Pair<>(clientLevel, chunkWrapper.getChunkPos()), chunkWrapper);
}
return;
@@ -31,7 +31,7 @@ import com.seibel.distanthorizons.core.config.types.enums.*;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.util.NativeDialogUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftSharedWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.core.logging.DhLogger;
@@ -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();
@@ -1210,9 +1241,11 @@ public class Config
});
public static void onButtonPressed()
{
IMinecraftClientWrapper mcClient = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
LOGGER.info("Attempting to show tinyfd message box...");
boolean buttonPress = NativeDialogUtil.showDialog("Button pressed!", "UITester dialog", "ok", "info");
LOGGER.info("dialog returned with ["+(buttonPress ? "TRUE" : "FALSE")+"]");
mcClient.showDialog("Button pressed!", "UITester dialog", "ok", "info");
LOGGER.info("dialog closed");
}
public static ConfigCategory categoryTest = new ConfigCategory.Builder().set(CategoryTest.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
}
@@ -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,7 +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.util.ColorUtil;
import com.seibel.distanthorizons.coreapi.util.ColorUtil;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.RenderDataPointUtil;
import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnRenderView;
@@ -341,13 +341,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 +384,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 +431,6 @@ public class ColumnBox
newSegments.add(YSegmentUtil.encode(rangeEnd, endY, skyLight));
}
}
segments.clear();
segments.addAll(newSegments);
}
private static void tryAddVerticalFaceWithSkyLightToBuilder(
@@ -31,7 +31,7 @@ import com.seibel.distanthorizons.core.util.objects.pooling.PhantomArrayListChec
import com.seibel.distanthorizons.core.util.objects.pooling.PhantomArrayListPool;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
import com.seibel.distanthorizons.core.util.ColorUtil;
import com.seibel.distanthorizons.coreapi.util.ColorUtil;
import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.RenderDataPointUtil;
import com.seibel.distanthorizons.core.dataObjects.render.columnViews.ColumnRenderView;
@@ -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
try
{
//=============//
// create VBOs //
//=============//
//region
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) ->
CompletableFuture<Void> createFuture = new CompletableFuture<Void>();
RenderThreadTaskHandler.INSTANCE.queueRunningOnRenderThread("LodBufferContainer Setup", () ->
{
// create VBOs failed //
if (!ExceptionUtil.isShutdownException(e))
try
{
LOGGER.error("Unexpected issue creating buffer [" + this.minCornerBlockPos + "], error: [" + e.getMessage() + "].", e);
// skip this event if requested
if (Thread.interrupted()
|| future.isCancelled())
{
throw new InterruptedException();
}
future.completeExceptionally(e);
return null;
});
createFuture.thenRun(() ->
{
//=============//
// upload VBOs //
//=============//
CompletableFuture<Void> opaqueFuture = uploadBuffersAsync(future, this.vboOpaqueWrappers, opaqueBuffers, opaqueIndexBuffers);
CompletableFuture<Void> transparentFuture = uploadBuffersAsync(future, this.vboTransparentWrappers, transparentBuffers, transparentIndexBuffers);
createBufferWrappers(bufferContainer.vboOpaqueWrappers, opaqueBuffers);
createBufferWrappers(bufferContainer.vboTransparentWrappers, transparentBuffers);
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);
});
});
createFuture.complete(null);
}
catch (Exception e)
{
if (!ExceptionUtil.isShutdownException(e))
{
LOGGER.error("Unexpected issue prepping buffer uploading [" + 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;
});
uploadFuture.thenRun(() ->
{
// upload success //
bufferContainer.buffersUploaded = true;
future.complete(bufferContainer);
});
});
//================//
// buffer cleanup //
//================//
//endregion
//====================//
// CPU Buffer cleanup //
//====================//
//region
future.whenComplete((LodBufferContainer lodBufferContainer, Throwable throwable) ->
{
@@ -259,9 +255,9 @@ public class LodBufferContainer implements AutoCloseable
{
ByteBuffer buffer = vertexBuffers.get(i);
int size = buffer.limit() - buffer.position();
int vertexCount = size / LodQuadBuilder.BYTES_PER_VERTEX;
ByteBuffer indexBuffer = IndexBufferBuilder.createBuffer(vertexCount);
int maxVertexCount = size / LodQuadBuilder.BYTES_PER_VERTEX;
int quadCount = (maxVertexCount / 4);
ByteBuffer indexBuffer = IndexBufferBuilder.createBuffer(quadCount);
indexBuffers.add(indexBuffer);
}
@@ -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);
}
}
catch (Exception 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
@@ -30,7 +30,7 @@ import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.enums.EDhDirection;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.util.ColorUtil;
import com.seibel.distanthorizons.coreapi.util.ColorUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import org.lwjgl.system.MemoryUtil;
@@ -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;
@@ -37,6 +37,7 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrappe
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IBiomeWrapper;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.coreapi.util.BitShiftUtil;
import com.seibel.distanthorizons.coreapi.util.ColorUtil;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import com.seibel.distanthorizons.core.logging.DhLogger;
@@ -128,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++)
@@ -141,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);
}
}
}
@@ -156,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
@@ -169,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
{
@@ -177,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);
}
@@ -185,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 //
@@ -241,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++)
@@ -449,9 +452,18 @@ public class FullDataToRenderDataTransformer
// use the previous block's color
color = colorToApplyToNextBlock;
colorToApplyToNextBlock = -1;
// use the skylight override if present
if (skylightToApplyToNextBlock != -1)
{
skyLight = skylightToApplyToNextBlock;
}
if (blocklightToApplyToNextBlock != -1)
{
blockLight = blocklightToApplyToNextBlock;
}
}
@@ -27,7 +27,7 @@ import com.seibel.distanthorizons.core.file.fullDatafile.V2.FullDataSourceProvid
import com.seibel.distanthorizons.core.util.delayedSaveCache.DelayedDataSourceSaveCache;
import com.seibel.distanthorizons.core.file.structure.ISaveStructure;
import com.seibel.distanthorizons.core.generation.DhLightingEngine;
import com.seibel.distanthorizons.core.generation.IFullDataSourceRetrievalQueue;
import com.seibel.distanthorizons.core.generation.queues.IFullDataSourceRetrievalQueue;
import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.generation.tasks.ERetrievalResultState;
import com.seibel.distanthorizons.core.level.IDhLevel;
@@ -22,10 +22,10 @@ package com.seibel.distanthorizons.core.file.fullDatafile;
import com.google.common.cache.CacheBuilder;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.file.structure.ISaveStructure;
import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue;
import com.seibel.distanthorizons.core.generation.queues.RemoteWorldRetrievalQueue;
import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.level.IDhLevel;
import com.seibel.distanthorizons.core.level.LodRequestModule;
import com.seibel.distanthorizons.core.generation.queues.LodRequestModule;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.generation.tasks.ERetrievalResultState;
import com.seibel.distanthorizons.core.multiplayer.client.SyncOnLoadRequestQueue;
@@ -0,0 +1,227 @@
package com.seibel.distanthorizons.core.generation.queues;
import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorProgressDisplayLocation;
import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.level.DhServerLevel;
import com.seibel.distanthorizons.core.level.IDhLevel;
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.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.util.FormatUtil;
import com.seibel.distanthorizons.core.util.ThreadUtil;
import com.seibel.distanthorizons.core.util.objects.RollingAverage;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
/**
* Handles the {@link IFullDataSourceRetrievalQueue} and any other necessary world gen information.
*
* @see LodRequestModule
* @see IFullDataSourceRetrievalQueue
*/
public abstract class AbstractLodRequestState
{
private static final DhLogger LOGGER = new DhLoggerBuilder().build();
/** static so we only send the disable message once per session */
private static long firstProgressMessageSentMs = 0;
public final IDhLevel dhLevel;
public final IFullDataSourceRetrievalQueue retrievalQueue;
private final ThreadPoolExecutor progressUpdaterThread = ThreadUtil.makeSingleDaemonThreadPool("World Gen Progress Updater");
private boolean progressUpdateThreadRunning = false;
//===========================//
// request queue and logging //
//===========================//
//region
public AbstractLodRequestState(IDhLevel dhLevel, IFullDataSourceRetrievalQueue retrievalQueue)
{
this.dhLevel = dhLevel;
this.retrievalQueue = retrievalQueue;
}
//endregion
//===========================//
// request queue and logging //
//===========================//
//region
/** @param targetPosForRequest the position that world generation should be centered around */
public void startRequestQueueAndSetTargetPos(DhBlockPos2D targetPosForRequest)
{
this.retrievalQueue.startAndSetTargetPos(targetPosForRequest);
this.startProgressUpdateThread();
}
private void startProgressUpdateThread()
{
// only start the thread once
if (!this.progressUpdateThreadRunning)
{
this.progressUpdateThreadRunning = true;
progressUpdaterThread.execute(() ->
{
while (this.progressUpdateThreadRunning)
{
try
{
this.sendRetrievalProgress();
// sleep so we only see an update once in a while
int sleepTimeInSec = Config.Common.WorldGenerator.generationProgressDisplayIntervalInSeconds.get();
Thread.sleep(sleepTimeInSec * 1_000L);
}
catch (Exception e)
{
LOGGER.error("Unexpected issue displaying chunk retrieval progress [" + e.getMessage() + "].", e);
}
}
});
}
}
private void sendRetrievalProgress()
{
// format the remaining chunks
int remainingChunkCount = this.retrievalQueue.getRetrievalEstimatedRemainingChunkCount();
remainingChunkCount += this.retrievalQueue.getQueuedChunkCount();
String remainingChunkCountStr = F3Screen.NUMBER_FORMAT.format(remainingChunkCount);
String message = "DH is generating chunks. ";
if (this.dhLevel.getClass() == DhServerLevel.class)
{
// server levels can have multiple world generators running at once,
// this helps us track of which queue is which
message += "For " + this.dhLevel.getLevelWrapper().getDimensionName() + " ";
}
message += remainingChunkCountStr + " left.";
// show a message about how to disable progress logging if requested
int msToShowDisableInstructions = Config.Common.WorldGenerator.generationProgressDisableMessageDisplayTimeInSeconds.get() * 1_000;
if (msToShowDisableInstructions > 0)
{
long timeSinceFirstMessageInMs = (System.currentTimeMillis() - firstProgressMessageSentMs);
// always show this message for the first tick
if (firstProgressMessageSentMs == 0
// show this message if there is still time
|| timeSinceFirstMessageInMs < msToShowDisableInstructions)
{
// append to the current message
message += " This message can be hidden in the DH config.";
}
}
// add the remaining time estimate if available
double chunksPerSec = this.getEstimatedChunksPerSecond();
if (chunksPerSec > 0)
{
long estimatedRemainingTime = (long) (remainingChunkCount / chunksPerSec);
message += " ETA: " + FormatUtil.formatEta(Duration.ofSeconds(estimatedRemainingTime));
if (Config.Common.WorldGenerator.generationProgressIncludeChunksPerSecond.get())
{
message += " at " + F3Screen.NUMBER_FORMAT.format(chunksPerSec) + " chunks/sec";
}
}
// only log if there are chunks needing to be generated
if (remainingChunkCount != 0)
{
// determine where to log
EDhApiDistantGeneratorProgressDisplayLocation displayLocation = Config.Common.WorldGenerator.showGenerationProgress.get();
if (displayLocation == EDhApiDistantGeneratorProgressDisplayLocation.OVERLAY)
{
ClientApi.INSTANCE.showOverlayMessageNextFrame(message);
}
else if (displayLocation == EDhApiDistantGeneratorProgressDisplayLocation.CHAT)
{
ClientApi.INSTANCE.showChatMessageNextFrame(message);
}
else if (displayLocation == EDhApiDistantGeneratorProgressDisplayLocation.LOG)
{
LOGGER.info(message);
}
// mark when the first message was sent
if (firstProgressMessageSentMs == 0)
{
firstProgressMessageSentMs = System.currentTimeMillis();
}
}
}
/** @return -1 if this method isn't supported or available */
public double getEstimatedChunksPerSecond()
{
RollingAverage avg = this.retrievalQueue.getRollingAverageChunkGenTimeInMs();
if (avg == null)
{
return -1;
}
PriorityTaskPicker.Executor executor = ThreadPoolUtil.getWorldGenExecutor();
int threadCount = 1;
if (executor != null)
{
threadCount = executor.getPoolSize();
}
// convert chunk generation time in milliseconds to chunks per second
double chunksPerSecond = (1 / avg.getAverage()) * 1_000;
// estimate the number of chunks that can be processed per second by all threads
// Note: this is probably higher than the actual number, we might want to drop this by 1 or 2 to give a more realistic estimate
chunksPerSecond = threadCount * chunksPerSecond;
return chunksPerSecond;
}
//endregion
//================//
// base overrides //
//================//
//region
public CompletableFuture<Void> closeAsync(boolean doInterrupt)
{
// this should stop the updater thread
this.progressUpdateThreadRunning = false;
return this.retrievalQueue.startClosingAsync(true, doInterrupt)
.exceptionally(e ->
{
LOGGER.error("Error during first stage of generation queue shutdown, Error: [" + e.getMessage() + "].", e);
return null;
}
).thenRun(this.retrievalQueue::close)
.exceptionally(e ->
{
LOGGER.error("Error during second stage of generation queue shutdown, Error: [" + e.getMessage() + "].", e);
return null;
});
}
//endregion
}
@@ -17,7 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.generation;
package com.seibel.distanthorizons.core.generation.queues;
import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
@@ -42,6 +42,8 @@ import java.util.concurrent.CompletableFuture;
* Used by both world gen and server networking.
*
* @see LodQuadTree
* @see AbstractLodRequestState
* @see LodRequestModule
*/
public interface IFullDataSourceRetrievalQueue extends Closeable
{
@@ -17,39 +17,35 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.level;
package com.seibel.distanthorizons.core.generation.queues;
import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorProgressDisplayLocation;
import com.seibel.distanthorizons.core.api.internal.ClientApi;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSourceProvider;
import com.seibel.distanthorizons.core.generation.IFullDataSourceRetrievalQueue;
import com.seibel.distanthorizons.core.level.*;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos2D;
import com.seibel.distanthorizons.core.util.FormatUtil;
import com.seibel.distanthorizons.core.util.ThreadUtil;
import com.seibel.distanthorizons.core.util.objects.RollingAverage;
import com.seibel.distanthorizons.core.util.threading.PriorityTaskPicker;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import com.seibel.distanthorizons.core.world.DhApiWorldProxy;
import com.seibel.distanthorizons.core.logging.DhLogger;
import java.io.Closeable;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
/**
* Handles both single-player/server-side world gen and client side LOD requests.
*
* @see AbstractLodRequestState
* @see IFullDataSourceRetrievalQueue
*/
public class LodRequestModule implements Closeable
{
private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private final IDhLevel level;
private final GeneratedFullDataSourceProvider.IOnWorldGenCompleteListener onWorldGenCompleteListener;
private final ThreadPoolExecutor tickerThread;
@@ -63,6 +59,7 @@ public class LodRequestModule implements Closeable
//=============//
// constructor //
//=============//
//region
public LodRequestModule(
IDhLevel level,
@@ -71,38 +68,73 @@ public class LodRequestModule implements Closeable
Supplier<? extends AbstractLodRequestState> worldGenStateSupplier
)
{
this.level = level;
this.onWorldGenCompleteListener = onWorldGenCompleteListener;
this.dataSourceProvider = dataSourceProvider;
this.worldGenStateSupplier = worldGenStateSupplier;
String levelId = level.getLevelWrapper().getDhIdentifier();
String levelId = this.level.getLevelWrapper().getDhIdentifier();
this.tickerThread = ThreadUtil.makeSingleDaemonThreadPool("Request Module Ticker ["+levelId+"]");
this.tickerThread.execute(this::tickLoop);
}
//endregion
//=========//
// ticking //
//=========//
//region
private void tickLoop()
{
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())
{
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) { }
}
private void tick()
{
boolean shouldDoWorldGen = this.onWorldGenCompleteListener.shouldDoWorldGen();
// if the world is read only don't generate anything
shouldDoWorldGen &= !DhApiWorldProxy.INSTANCE.getReadOnly();
shouldDoWorldGen &= !DhApiWorldProxy.INSTANCE.tryGetReadOnly();
// 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;
}
}
boolean isWorldGenRunning = this.isWorldGenRunning();
if (shouldDoWorldGen && !isWorldGenRunning)
@@ -130,11 +162,14 @@ public class LodRequestModule implements Closeable
}
}
//endregion
//===================//
// world gen control //
//===================//
//region
public void startWorldGen(GeneratedFullDataSourceProvider dataFileHandler, AbstractLodRequestState newWgs)
{
@@ -173,11 +208,14 @@ public class LodRequestModule implements Closeable
dataFileHandler.removeWorldGenCompleteListener(this.onWorldGenCompleteListener);
}
//endregion
//=======================//
// base method overrides //
//=======================//
//region
@Override
public void close()
@@ -205,11 +243,14 @@ public class LodRequestModule implements Closeable
}
}
//endregion
//=========//
// getters //
//=========//
//region
public boolean isWorldGenRunning() { return this.lodRequestStateRef.get() != null; }
@@ -241,170 +282,7 @@ public class LodRequestModule implements Closeable
worldGenState.retrievalQueue.addDebugMenuStringsToList(messageList);
}
//================//
// helper classes //
//================//
/** Handles the {@link IFullDataSourceRetrievalQueue} and any other necessary world gen information. */
public static abstract class AbstractLodRequestState
{
/** static so we only send the disable message once per session */
private static long firstProgressMessageSentMs = 0;
public IFullDataSourceRetrievalQueue retrievalQueue;
private static final ThreadPoolExecutor PROGRESS_UPDATER_THREAD = ThreadUtil.makeSingleDaemonThreadPool("World Gen Progress Updater");
private boolean progressUpdateThreadRunning = false;
/** @param targetPosForRequest the position that world generation should be centered around */
public void startRequestQueueAndSetTargetPos(DhBlockPos2D targetPosForRequest)
{
this.retrievalQueue.startAndSetTargetPos(targetPosForRequest);
this.startProgressUpdateThread();
}
private void startProgressUpdateThread()
{
// only start the thread once
if (!this.progressUpdateThreadRunning)
{
this.progressUpdateThreadRunning = true;
PROGRESS_UPDATER_THREAD.execute(() ->
{
while (this.progressUpdateThreadRunning)
{
try
{
this.sendRetrievalProgress();
// sleep so we only see an update once in a while
int sleepTimeInSec = Config.Common.WorldGenerator.generationProgressDisplayIntervalInSeconds.get();
Thread.sleep(sleepTimeInSec * 1_000L);
}
catch (Exception e)
{
LOGGER.error("Unexpected issue displaying chunk retrieval progress [" + e.getMessage() + "].", e);
}
}
});
}
}
private void sendRetrievalProgress()
{
// format the remaining chunks
int remainingChunkCount = this.retrievalQueue.getRetrievalEstimatedRemainingChunkCount();
remainingChunkCount += this.retrievalQueue.getQueuedChunkCount();
String remainingChunkCountStr = F3Screen.NUMBER_FORMAT.format(remainingChunkCount);
String message = "DH is generating chunks. " + remainingChunkCountStr + " left."; // TODO getting stuck at 32 chunks remaining
// show a message about how to disable progress logging if requested
int msToShowDisableInstructions = Config.Common.WorldGenerator.generationProgressDisableMessageDisplayTimeInSeconds.get() * 1_000;
if (msToShowDisableInstructions > 0)
{
long timeSinceFirstMessageInMs = (System.currentTimeMillis() - firstProgressMessageSentMs);
// always show this message for the first tick
if (firstProgressMessageSentMs == 0
// show this message if there is still time
|| timeSinceFirstMessageInMs < msToShowDisableInstructions)
{
// append to the current message
message += " This message can be hidden in the DH config.";
}
}
// add the remaining time estimate if available
double chunksPerSec = this.getEstimatedChunksPerSecond();
if (chunksPerSec > 0)
{
long estimatedRemainingTime = (long) (remainingChunkCount / chunksPerSec);
message += " ETA: " + FormatUtil.formatEta(Duration.ofSeconds(estimatedRemainingTime));
if (Config.Common.WorldGenerator.generationProgressIncludeChunksPerSecond.get())
{
message += " at " + F3Screen.NUMBER_FORMAT.format(chunksPerSec) + " chunks/sec";
}
}
// only log if there are chunks needing to be generated
if (remainingChunkCount != 0)
{
// determine where to log
EDhApiDistantGeneratorProgressDisplayLocation displayLocation = Config.Common.WorldGenerator.showGenerationProgress.get();
if (displayLocation == EDhApiDistantGeneratorProgressDisplayLocation.OVERLAY)
{
ClientApi.INSTANCE.showOverlayMessageNextFrame(message);
}
else if (displayLocation == EDhApiDistantGeneratorProgressDisplayLocation.CHAT)
{
ClientApi.INSTANCE.showChatMessageNextFrame(message);
}
else if (displayLocation == EDhApiDistantGeneratorProgressDisplayLocation.LOG)
{
LOGGER.info(message);
}
// mark when the first message was sent
if (firstProgressMessageSentMs == 0)
{
firstProgressMessageSentMs = System.currentTimeMillis();
}
}
}
/** @return -1 if this method isn't supported or available */
public double getEstimatedChunksPerSecond()
{
RollingAverage avg = this.retrievalQueue.getRollingAverageChunkGenTimeInMs();
if (avg == null)
{
return -1;
}
PriorityTaskPicker.Executor executor = ThreadPoolUtil.getWorldGenExecutor();
int threadCount = 1;
if (executor != null)
{
threadCount = executor.getPoolSize();
}
// convert chunk generation time in milliseconds to chunks per second
double chunksPerSecond = (1 / avg.getAverage()) * 1_000;
// estimate the number of chunks that can be processed per second by all threads
// Note: this is probably higher than the actual number, we might want to drop this by 1 or 2 to give a more realistic estimate
chunksPerSecond = threadCount * chunksPerSecond;
return chunksPerSecond;
}
CompletableFuture<Void> closeAsync(boolean doInterrupt)
{
// this should stop the updater thread
this.progressUpdateThreadRunning = false;
return this.retrievalQueue.startClosingAsync(true, doInterrupt)
.exceptionally(e ->
{
LOGGER.error("Error during first stage of generation queue shutdown, Error: ["+e.getMessage()+"].", e);
return null;
}
).thenRun(this.retrievalQueue::close)
.exceptionally(e ->
{
LOGGER.error("Error during second stage of generation queue shutdown, Error: ["+e.getMessage()+"].", e);
return null;
});
}
}
//endregion
@@ -1,4 +1,4 @@
package com.seibel.distanthorizons.core.generation;
package com.seibel.distanthorizons.core.generation.queues;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.file.fullDatafile.V2.FullDataSourceProviderV2;
@@ -15,7 +15,6 @@ import com.seibel.distanthorizons.core.util.LodUtil;
import com.seibel.distanthorizons.core.util.WorldGenUtil;
import com.seibel.distanthorizons.core.util.objects.RollingAverage;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.util.threading.ThreadPoolUtil;
import java.util.concurrent.*;
@@ -17,7 +17,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.seibel.distanthorizons.core.generation;
package com.seibel.distanthorizons.core.generation.queues;
import com.seibel.distanthorizons.api.enums.worldGeneration.EDhApiDistantGeneratorMode;
import com.seibel.distanthorizons.api.interfaces.override.worldGenerator.IDhApiWorldGenerator;
@@ -27,6 +27,7 @@ import com.seibel.distanthorizons.api.objects.data.IDhApiFullDataSource;
import com.seibel.distanthorizons.core.dataObjects.fullData.sources.FullDataSourceV2;
import com.seibel.distanthorizons.core.dependencyInjection.SingletonInjector;
import com.seibel.distanthorizons.core.file.fullDatafile.V2.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.generation.DhLightingEngine;
import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalTask;
import com.seibel.distanthorizons.core.level.IDhServerLevel;
@@ -201,7 +202,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb
private synchronized void tryQueueNewWorldGenRequestsAsync()
{
if (!DhApiWorldProxy.INSTANCE.worldLoaded()
|| DhApiWorldProxy.INSTANCE.getReadOnly())
|| DhApiWorldProxy.INSTANCE.tryGetReadOnly())
{
return;
}
@@ -94,7 +94,10 @@ public class GitlabGetter
{
try
{
pipelineInfo.put(pipeline, WebDownloader.parseWebJsonList(this.GitProjID + "pipelines/" + pipeline + "/jobs"));
pipelineInfo.put(pipeline, WebDownloader.parseWebJsonList(this.GitProjID + "pipelines/" + pipeline + "/jobs?per_page=100"));
// max page size is 100,
// if we ever support more than 100 MC versions this logic will need to handle pagination
// https://docs.gitlab.com/api/rest/#pagination
}
catch (Exception e)
{
@@ -28,16 +28,13 @@ import com.seibel.distanthorizons.core.jar.installer.GitlabGetter;
import com.seibel.distanthorizons.core.jar.installer.ModrinthGetter;
import com.seibel.distanthorizons.core.jar.installer.WebDownloader;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.logging.f3.F3Screen;
import com.seibel.distanthorizons.core.util.NativeDialogUtil;
import com.seibel.distanthorizons.core.wrapperInterfaces.IVersionConstants;
import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftClientWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.coreapi.util.StringUtil;
import com.seibel.distanthorizons.coreapi.util.jar.DeleteOnUnlock;
import com.seibel.distanthorizons.core.logging.DhLogger;
import javax.swing.*;
import java.awt.*;
import java.io.*;
import java.net.URLEncoder;
import java.nio.file.Files;
@@ -59,6 +56,8 @@ public class SelfUpdater
{
private static final DhLogger LOGGER = new DhLoggerBuilder().build();
private static final IMinecraftClientWrapper MC_CLIENT = SingletonInjector.INSTANCE.get(IMinecraftClientWrapper.class);
/** As we cannot delete(or replace) the jar while the mod is running, we just have this to delete it once the game closes */
public static boolean deleteOldJarOnJvmShutdown = false;
@@ -264,7 +263,7 @@ public class SelfUpdater
{
try
{
NativeDialogUtil.showDialog(ModInfo.READABLE_NAME, successMessage, "ok", "info");
MC_CLIENT.showDialog(ModInfo.READABLE_NAME, successMessage, "ok", "info");
}
catch (Exception ignore) { }
}).start();
@@ -287,7 +286,7 @@ public class SelfUpdater
LOGGER.error(failMessage, e);
try
{
NativeDialogUtil.showDialog(ModInfo.READABLE_NAME, failMessage, "ok", "error");
MC_CLIENT.showDialog(ModInfo.READABLE_NAME, failMessage, "ok", "error");
}
catch (Exception ignore) { }
@@ -386,7 +385,7 @@ public class SelfUpdater
{
try
{
NativeDialogUtil.showDialog(ModInfo.READABLE_NAME, successMessage, "ok", "info");
MC_CLIENT.showDialog(ModInfo.READABLE_NAME, successMessage, "ok", "info");
}
catch (Exception ignore) { }
}).start();
@@ -424,7 +423,7 @@ public class SelfUpdater
LOGGER.error(failMessage, e);
try
{
NativeDialogUtil.showDialog(ModInfo.READABLE_NAME, failMessage, "ok", "error");
MC_CLIENT.showDialog(ModInfo.READABLE_NAME, failMessage, "ok", "error");
}
catch (Exception ignore) { }
@@ -29,8 +29,10 @@ 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.render.renderPass.IDhGenericRenderer;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.core.logging.DhLogger;
@@ -44,6 +46,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 +109,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 +176,7 @@ public class ClientLevelModule implements Closeable, IDataSourceUpdateListenerFu
}
this.fullDataSourceProvider.removeDataSourceUpdateListener(this);
this.genericRenderer.close();
}
@@ -259,7 +266,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();
}
@@ -27,7 +27,9 @@ import com.seibel.distanthorizons.core.enums.MinecraftTextFormat;
import com.seibel.distanthorizons.core.file.fullDatafile.V2.FullDataSourceProviderV2;
import com.seibel.distanthorizons.core.file.fullDatafile.RemoteFullDataSourceProvider;
import com.seibel.distanthorizons.core.file.structure.ISaveStructure;
import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue;
import com.seibel.distanthorizons.core.generation.queues.RemoteWorldRetrievalQueue;
import com.seibel.distanthorizons.core.generation.queues.AbstractLodRequestState;
import com.seibel.distanthorizons.core.generation.queues.LodRequestModule;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.multiplayer.client.ClientNetworkState;
@@ -396,12 +398,10 @@ public class DhClientLevel extends AbstractDhLevel implements IDhClientLevel
//================//
//region
private static class LodRequestState extends LodRequestModule.AbstractLodRequestState
private static class LodRequestState extends AbstractLodRequestState
{
LodRequestState(DhClientLevel clientLevel, ClientNetworkState networkState)
{
this.retrievalQueue = new RemoteWorldRetrievalQueue(networkState, clientLevel);
}
{ super(clientLevel, new RemoteWorldRetrievalQueue(networkState, clientLevel)); }
}
//endregion
@@ -23,7 +23,10 @@ import com.seibel.distanthorizons.api.interfaces.override.worldGenerator.IDhApiW
import com.seibel.distanthorizons.core.file.fullDatafile.GeneratedFullDataSourceProvider;
import com.seibel.distanthorizons.core.file.structure.ISaveStructure;
import com.seibel.distanthorizons.core.generation.BatchGenerator;
import com.seibel.distanthorizons.core.generation.WorldGenerationQueue;
import com.seibel.distanthorizons.core.generation.queues.IFullDataSourceRetrievalQueue;
import com.seibel.distanthorizons.core.generation.queues.WorldGenerationQueue;
import com.seibel.distanthorizons.core.generation.queues.AbstractLodRequestState;
import com.seibel.distanthorizons.core.generation.queues.LodRequestModule;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.coreapi.DependencyInjection.WorldGeneratorInjector;
import com.seibel.distanthorizons.core.logging.DhLogger;
@@ -75,9 +78,11 @@ public class ServerLevelModule implements AutoCloseable
// helper classes //
//================//
public static class LodRequestState extends LodRequestModule.AbstractLodRequestState
public static class LodRequestState extends AbstractLodRequestState
{
LodRequestState(IDhServerLevel level)
{ super(level, createRetrievalQueue(level)); }
private static IFullDataSourceRetrievalQueue createRetrievalQueue(IDhServerLevel level)
{
IDhApiWorldGenerator worldGenerator = WorldGeneratorInjector.INSTANCE.get(level.getLevelWrapper());
if (worldGenerator == null)
@@ -88,9 +93,11 @@ public class ServerLevelModule implements AutoCloseable
// since core world generator's should have the lowest override priority
WorldGeneratorInjector.INSTANCE.bind(level.getLevelWrapper(), worldGenerator);
}
this.retrievalQueue = new WorldGenerationQueue(worldGenerator, level);
return new WorldGenerationQueue(worldGenerator, level);
}
}
}
@@ -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;
}
@@ -1,7 +1,7 @@
package com.seibel.distanthorizons.core.multiplayer.client;
import com.seibel.distanthorizons.core.config.Config;
import com.seibel.distanthorizons.core.generation.RemoteWorldRetrievalQueue;
import com.seibel.distanthorizons.core.generation.queues.RemoteWorldRetrievalQueue;
import com.seibel.distanthorizons.core.generation.tasks.DataSourceRetrievalResult;
import com.seibel.distanthorizons.core.level.DhClientLevel;
import com.seibel.distanthorizons.core.pos.DhSectionPos;
@@ -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
{
@@ -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) ->
// remove out of bound sections
this.setCenterBlockPos(playerPos,
// remove completely out of bound nodes
// (the root node is no longer in bounds)
(renderSection) ->
{
// removing out of bounds sections
if (renderSection != null)
{
this.fullDataSourceProvider.removeRetrievalRequestIf((long genPos) -> DhSectionPos.contains(renderSection.pos, genPos));
this.missingGenerationPosSet.remove(renderSection.pos);
this.queuedGenerationPosSet.remove(renderSection.pos);
// 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)
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))
{
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+"].");
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(
QuadNode<LodRenderSection> childNode = quadNode.getChildByIndex(i);
boolean childCanRender = 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)
// nodes shouldn't be null, but just in case
if (childNode != null
&& childNode.value != null
&& !childNode.value.gpuUploadComplete())
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
{
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);
}
}
private void onDesiredDetailLevel(
@NotNull QuadNode<LodRenderSection> quadNode, @Nullable QuadNode<LodRenderSection> parentNode)
else
{
boolean allAdjNodesCanRender = true;
this.tickNodeHolder.addDisableNode(quadNode);
}
// if the parent node is null, that means we're at the root node
// and we should always render
if (parentNode != null)
{
// 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++)
{
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;
}
return nodeCanRender;
}
}
if (allAdjNodesCanRender
&& quadNode.value != null
&& quadNode.value.gpuUploadComplete())
/** @return true if the node at this position has uploaded its render data */
private boolean onDesiredDetailLevel(
@NotNull QuadNode<LodRenderSection> quadNode,
@Nullable QuadNode<LodRenderSection> parentNode)
{
// 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))
{
return true;
}
if (quadNode.value != null
&& quadNode.value.canRender())
{
if (!this.tickNodeHolder.getEnabledNodes().contains(parentNode))
{
this.tickNodeHolder.addEnableDeleteChildrenNode(quadNode);
return true;
}
else
{
this.tickNodeHolder.addDisableNode(quadNode);
return false;
}
}
else
{
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);
try
{
// 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);
}
CompletableFuture<LodBufferContainer> future = ColumnRenderBufferBuilder.uploadBuffersAsync(this.level, this.pos, lodQuadBuilder);
future.handle((lodBufferContainer, throwable) ->
if (bufferContainer != null)
{
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())
{
LOGGER.warn("Buffer upload future ref changed for pos: ["+DhSectionPos.toString(this.pos)+"].");
// shouldn't happen, but just in case
bufferContainer.close();
}
return;
}
return null;
});
future.thenAccept((LodBufferContainer buffer) ->
// close the old container
LodBufferContainer oldContainer = this.renderBufferContainer;
this.renderBufferContainer = bufferContainer.buffersUploaded ? bufferContainer : null;
if (oldContainer != null)
{
// needed to clean up the old data
LodBufferContainer previousContainer = this.renderBufferContainer;
oldContainer.close();
}
// upload complete
this.renderBufferContainer = buffer.buffersUploaded ? buffer : null;
this.renderDataDirty = false;
if (previousContainer != null)
if (parentFuture.isCancelled())
{
previousContainer.close();
// 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();
}
}
catch (Exception finishEx)
{
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();
}
}
@@ -62,6 +62,12 @@ public class QuadTreeTickNodeHolder
public void addEnableNode(QuadNode<LodRenderSection> node)
{
if(this.presentNodes.add(node))
{
// 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)
{
// not a big fan of having to check every node to prevent overlaps, but it does work
this.nodesToEnable.removeIf((QuadNode<LodRenderSection> checkNode) ->
@@ -74,10 +80,22 @@ public class QuadTreeTickNodeHolder
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; }
@@ -4,6 +4,7 @@ import com.seibel.distanthorizons.api.enums.rendering.EDhApiRenderPass;
import com.seibel.distanthorizons.api.methods.events.sharedParameterObjects.DhApiRenderParam;
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;
@@ -17,6 +18,7 @@ import com.seibel.distanthorizons.core.wrapperInterfaces.minecraft.IMinecraftCli
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;
@@ -30,6 +32,8 @@ public class RenderParams extends DhApiRenderParam
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 IOptifineAccessor OPTIFINE_ACCESSOR = ModAccessorInjector.INSTANCE.get(IOptifineAccessor.class);
private static final long TIME_FOR_MAC_TO_FINISH_COMPILING_IN_MS = 10_000;
private static boolean initialLoadingComplete = false;
@@ -164,7 +168,7 @@ public class RenderParams extends DhApiRenderParam
return "No MVM or Proj Matrix Given";
}
if (AbstractOptifineAccessor.optifinePresent()
if (OPTIFINE_ACCESSOR != null
&& MC_RENDER.getTargetFramebuffer() == -1)
{
// wait for MC to finish setting up their renderer
@@ -172,45 +176,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.
@@ -143,7 +146,8 @@ public class LodRenderer
//region
ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeRenderSetupEvent.class, renderParams);
profiler.push("LOD GL setup");
try (IProfilerWrapper.IProfileBlock terrainRender_profile = profiler.push("LOD GL setup")) // starts the new profile block for most DH rendering
{
if (!this.renderersBound)
{
@@ -179,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
@@ -188,8 +210,13 @@ public class LodRenderer
//===========//
if (!runningDeferredPass)
{
// needs to be fired after all the textures have been created/bound
boolean clearTextures = !ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeTextureClearEvent.class, renderParams);
if (clearTextures)
{
this.metaRenderer.clearDhDepthAndColorTextures(renderParams);
}
@@ -241,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");
@@ -265,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 //
@@ -290,9 +332,7 @@ public class LodRenderer
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");
@@ -311,11 +351,7 @@ public class LodRenderer
ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeRenderCleanupEvent.class, renderParams);
this.metaRenderer.runRenderPassCleanup(renderParams);
// end of internal LOD profiling
profiler.pop();
}
}
//endregion
@@ -333,8 +369,6 @@ public class LodRenderer
// rendering //
//===========//
ApiEventInjector.INSTANCE.fireAllEvents(DhApiBeforeRenderPassEvent.class, renderEventParam);
SortedArraySet<LodBufferContainer> lodBufferContainer = lodBufferHandler.getColumnRenderBuffers();
if (lodBufferContainer != null)
{
@@ -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; }
}
@@ -22,6 +22,7 @@ package com.seibel.distanthorizons.core.util;
import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.coreapi.util.ColorUtil;
/**
* This class holds methods and constants that may be used in multiple places.
@@ -1,30 +0,0 @@
package com.seibel.distanthorizons.core.util;
import org.lwjgl.util.tinyfd.TinyFileDialogs;
/**
* Should be used instead of the direct call to {@link TinyFileDialogs}
* so we can run additional validation and/or string cleanup.
* Otherwise, we may get error messages back.
*
* @see TinyFileDialogs
*/
public class NativeDialogUtil
{
/**
* @param dialogType the dialog type. One of:<br><table><tr><td>"ok"</td><td>"okcancel"</td><td>"yesno"</td><td>"yesnocancel"</td></tr></table>
* @param iconType the icon type. One of:<br><table><tr><td>"info"</td><td>"warning"</td><td>"error"</td><td>"question"</td></tr></table>
*/
public static boolean showDialog(String title, String message, String dialogType, String iconType)
{
// Tinyfd doesn't support the following characters, attempting to display them will cause the message
// to be replaced with an error message
String unsafeCharsRegex = "['\"`]";
title = title.replaceAll(unsafeCharsRegex, "`");
message = message.replaceAll(unsafeCharsRegex, "`");
return TinyFileDialogs.tinyfd_messageBox(title, message, dialogType, iconType, false);
}
}
@@ -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);
@@ -24,6 +24,7 @@ import com.seibel.distanthorizons.core.logging.DhLoggerBuilder;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.ILevelWrapper;
import com.seibel.distanthorizons.coreapi.ModInfo;
import com.seibel.distanthorizons.core.logging.DhLogger;
import com.seibel.distanthorizons.coreapi.util.ColorUtil;
/**
@@ -246,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;
}
@@ -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,20 +140,24 @@ 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();
QuadNode<T> node = leafNodeIterator.next();
if (node != null
&& this.isSectionPosInBounds(node.sectionPos))
{
count++;
}
}
}
return 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) ->
{
if (quadNode != null && removedItemConsumer != null)
{
quadNode.deleteAllChildren(removedItemConsumer);
removedItemConsumer.accept(quadNode.value);
this.centerBlockPos = newCenterPos;
// remove out of bound root nodes
this.topRingList.moveTo(newCenterPosX, newCenterPosZ, (quadNode) ->
{
if (quadNode != null)
{
quadNode.deleteAllChildren(removedConsumer);
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)
{
@@ -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;
}
//================//
@@ -20,6 +20,7 @@
package com.seibel.distanthorizons.core.wrapperInterfaces.block;
import com.seibel.distanthorizons.api.interfaces.block.IDhApiBlockStateWrapper;
import com.seibel.distanthorizons.api.methods.events.abstractEvents.DhApiBlockColorOverrideEvent;
import com.seibel.distanthorizons.core.util.LodUtil;
import java.awt.*;
@@ -59,6 +60,11 @@ public interface IBlockStateWrapper extends IDhApiBlockStateWrapper
* IE Iron, diamond, gold, etc.
*/
boolean isBeaconBaseBlock();
/**
* if true this block can have its color overridden
* by {@link DhApiBlockColorOverrideEvent}
*/
boolean allowApiColorOverride();
Color getMapColor();
Color getBeaconTintColor();
@@ -21,6 +21,7 @@ package com.seibel.distanthorizons.core.wrapperInterfaces.minecraft;
import com.seibel.distanthorizons.core.pos.blockPos.DhBlockPos;
import com.seibel.distanthorizons.core.pos.DhChunkPos;
import com.seibel.distanthorizons.core.render.RenderThreadTaskHandler;
import com.seibel.distanthorizons.core.wrapperInterfaces.world.IClientLevelWrapper;
import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable;
@@ -113,17 +114,19 @@ public interface IMinecraftClientWrapper extends IBindable
void crashMinecraft(String errorMessage, Throwable exception);
/**
* This is only designed to be used internally by {@link GLProxy}
* This is only designed to be used internally by {@link RenderThreadTaskHandler}
* since it handles task frame limiting (reducing/preventing stuttering)
* whereas this method causes the task to be run whenever MC decides to
* (likely all at once the next frame). <br><br>
*
* Any tasks submitted here will be run on the render thread.
*
* @see GLProxy#queueRunningOnRenderThread(Runnable)
* @see RenderThreadTaskHandler
*/
void executeOnRenderThread(Runnable runnable);
void showDialog(String title, String message, String dialogType, String iconType);
//=============//
@@ -21,20 +21,26 @@ package com.seibel.distanthorizons.core.wrapperInterfaces.minecraft;
import com.seibel.distanthorizons.coreapi.interfaces.dependencyInjection.IBindable;
/**
* @author James Seibel
* @version 11-20-2021
*/
public interface IProfilerWrapper extends IBindable
{
// Note to self:
// if "unspecified" shows up in the pie chart, it is
// possibly because the amount of time between sections
// is too small for the profiler to measures
void push(String newSection);
IProfileBlock push(String newSection);
void popPush(String newSection);
void pop();
//================//
// helper classes //
//================//
//region
/** used to auto-pop blocks to prevent accidentally unevenly pushing/popping */
public static interface IProfileBlock extends AutoCloseable
{
@Override
public void close();
}
//endregion
}
@@ -70,8 +70,6 @@ public abstract class AbstractOptifineAccessor implements IOptifineAccessor
return null;
}
public static boolean optifinePresent() { return getOptifineFogField() != null; }
//===================//
@@ -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"
}
@@ -533,6 +533,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",
@@ -41,8 +41,8 @@ void main()
// the DH texture will have white if nothing was written to that pixel.
if (dhColor == vec4(1))
// ignore anything that DH hasn't drawn to
if (dhColor.a == 0.0f)
{
// if not done vanilla clouds will render incorrectly at night
dhColor = combinedMcDhColor;
@@ -28,13 +28,36 @@ layout (std140) uniform fragUniformBlock
vec3 calcViewPosition(float fragmentDepth, mat4 invMvmProj)
{
// normalized device coordinates
vec4 ndc = vec4(TexCoord.xy, fragmentDepth, 1.0);
vec4 ndc = vec4(
TexCoord.x, // UV [0,1]
TexCoord.y,
fragmentDepth, // depth [0,1]
1.0
);
// AKA: remap the [0,1] UV coordinates and depth value
// into the [-1,1] positions used by
// the NDC cube rendering stage
ndc.xyz = ndc.xyz * 2.0 - 1.0;
vec4 eyeCoord = invMvmProj * ndc;
return eyeCoord.xyz / eyeCoord.w;
}
// TODO vulkan
vec3 calcReversedZViewPosition(float fragmentDepth, mat4 invMvmProj)
{
// Vulkan NDC: xy in [-1,+1], z already in [0,1] — don't remap z
vec4 ndc = vec4(
TexCoord.x * 2.0 - 1.0, // UV [0,1] -> NDC [-1,+1]
TexCoord.y * 2.0 - 1.0,
fragmentDepth, // no remapping needed, this depth is already in the [0,1] range
1.0 // w=1 placeholder for matrix multiplication
);
vec4 eyeCoord = invMvmProj * ndc;
return eyeCoord.xyz / eyeCoord.w;
}
/**
* Used to fade out vanilla chunks so the transition
* between DH and vanilla is smoother.
@@ -56,8 +79,7 @@ void main()
// ignore anything that DH hasn't drawn to
// We don't use DH's depth here because it would prevent the fade from running before DH has loaded
if (dhColor == vec4(1))
if (dhColor.a == 0.0f)
{
// if not done vanilla clouds will render incorrectly at night
dhColor = combinedMcDhColor;
@@ -72,12 +94,13 @@ void main()
{
fragColor = vec4(combinedMcDhColor.rgb, 0.0);
}
// a fragment depth of "1" means the fragment wasn't drawn to,
// we only want to fade vanilla rendered objects, not to the sky or LODs
else if (mcFragmentDepth < 1.0)
// // a fragment depth of "1" means the fragment wasn't drawn to,
// // we only want to fade vanilla rendered objects, not to the sky or LODs
// else if (mcFragmentDepth < 1.0)
else if (mcFragmentDepth > 0)
{
// fade based on distance from the camera
vec3 mcVertexWorldPos = calcViewPosition(mcFragmentDepth, uMcInvMvmProj);
vec3 mcVertexWorldPos = calcReversedZViewPosition(mcFragmentDepth, uMcInvMvmProj);
float mcFragmentDistance = length(mcVertexWorldPos.xzy);
@@ -1,7 +1,6 @@
#version 150 core
in vec2 vPosition;
in vec4 vColor;
out vec4 fColor;
out vec2 TexCoord;
@@ -39,8 +39,8 @@ void main()
// the DH texture will have white if nothing was written to that pixel.
if (dhColor == vec4(1))
// ignore anything that DH hasn't drawn to
if (dhColor.a == 0.0f)
{
// if not done vanilla clouds will render incorrectly at night
dhColor = combinedMcDhColor;
@@ -52,8 +52,7 @@ void main()
// ignore anything that DH hasn't drawn to
// We don't use DH's depth here because it would prevent the fade from running before DH has loaded
if (dhColor == vec4(1))
if (dhColor.a == 0.0f)
{
// if not done vanilla clouds will render incorrectly at night
dhColor = combinedMcDhColor;
@@ -113,12 +113,13 @@ void main()
float fadeDistance = uFadeDistanceInBlocks;
if (distanceFromCamera < fadeDistance)
{
#ifdef GL_ARB_derivative_control
// Get higher precision derivatives when available
vec3 viewNormal = cross(dFdxFine(viewPos.xyz), dFdyFine(viewPos.xyz));
#else
// TODO vulkan
// #ifdef GL_ARB_derivative_control
// // Get higher precision derivatives when available
// vec3 viewNormal = cross(dFdxFine(viewPos.xyz), dFdyFine(viewPos.xyz));
// #else
vec3 viewNormal = cross(dFdx(viewPos.xyz), dFdy(viewPos.xyz));
#endif
// #endif
viewNormal = normalize(viewNormal);
occlusion = GetSpiralOcclusion(TexCoord, viewPos, viewNormal);
@@ -14,49 +14,22 @@ public class TestBlockStateWrapper implements IBlockStateWrapper
@Override
public boolean isAir()
{ return false; }
@Override
public boolean isSolid()
{ return true; }
@Override
public boolean isLiquid()
{ return false; }
@Override
public String getSerialString()
{ return this.name; }
@Override
public int getOpacity()
{ return 15; }
@Override
public int getLightEmission()
{ return 0; }
@Override
public byte getMaterialId()
{ return 0; }
@Override
public boolean isBeaconBlock()
{ return false; }
@Override
public boolean isBeaconTintBlock()
{ return false; }
@Override
public boolean allowsBeaconBeamPassage()
{ return false; }
@Override
public boolean isBeaconBaseBlock()
{ return false; }
@Override
public Color getMapColor()
{ return Color.MAGENTA; }
@Override
public Color getBeaconTintColor()
{ return Color.MAGENTA; }
@Override public boolean isAir() { return false; }
@Override public boolean isSolid() { return true; }
@Override public boolean isLiquid() { return false; }
@Override public String getSerialString() { return this.name; }
@Override public int getOpacity() { return 15; }
@Override public int getLightEmission() { return 0; }
@Override public byte getMaterialId() { return 0; }
@Override public boolean isBeaconBlock() { return false; }
@Override public boolean isBeaconTintBlock() { return false; }
@Override public boolean allowsBeaconBeamPassage() { return false; }
@Override public boolean isBeaconBaseBlock() { return false; }
@Override public boolean allowApiColorOverride() { return false; }
@Override public Color getMapColor() { return Color.MAGENTA; }
@Override public Color getBeaconTintColor() { return Color.MAGENTA; }
@Override
public Object getWrappedMcObject()
{ return this; }
@Override public Object getWrappedMcObject() { return this; }
@Override
+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; }
}