diff --git a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/override/worldGenerator/IDhApiWorldGenerator.java b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/override/worldGenerator/IDhApiWorldGenerator.java index 7861040cc..4419b8686 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/interfaces/override/worldGenerator/IDhApiWorldGenerator.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/interfaces/override/worldGenerator/IDhApiWorldGenerator.java @@ -90,11 +90,29 @@ public interface IDhApiWorldGenerator extends Closeable, IDhApiOverrideable */ default byte getMaxGenerationGranularity() { return (byte) (EDhApiDetailLevel.CHUNK.detailLevel + 2); } - /** - * @return true if the generator is unable to accept new generation requests. + /** + * Starting in API 3.0.0 DH now handles future queuing/management internally.

+ * + * Previous description:
+ * true if the generator is unable to accept new generation requests.
+ * * @since API 1.0.0 + * @deprecated API 3.0.0 */ - boolean isBusy(); + @Deprecated + default boolean isBusy() { return false; } + + /** + * Only used if {@link #getReturnType()} returns {@link EDhApiWorldGeneratorReturnType#API_CHUNKS}.
+ * If true DH will run additional validation on the {@link DhApiChunk}'s returned.
+ * This should be disabled during release but should be enabled during development to help spot issues with your data format. + * + * @see #getReturnType() + * @see DhApiChunk + * @see EDhApiWorldGeneratorReturnType#API_CHUNKS + * @since API 3.0.0 + */ + default boolean runApiChunkValidation() { return true; } diff --git a/api/src/main/java/com/seibel/distanthorizons/api/objects/data/DhApiChunk.java b/api/src/main/java/com/seibel/distanthorizons/api/objects/data/DhApiChunk.java index fd8a064c9..04e8a3bc9 100644 --- a/api/src/main/java/com/seibel/distanthorizons/api/objects/data/DhApiChunk.java +++ b/api/src/main/java/com/seibel/distanthorizons/api/objects/data/DhApiChunk.java @@ -116,32 +116,19 @@ public class DhApiChunk */ public void setDataPoints(int relX, int relZ, List dataPoints) throws IndexOutOfBoundsException, IllegalArgumentException { - //================// - // validate input // - //================// + //==================// + // basic validation // + //==================// + + // heavier validation is done in the world generator if requested int internalArrayIndex = (relZ << 4) | relX; throwIfRelativePosOutOfBounds(relX, relZ); - // ignore empty inputs if (dataPoints == null) { - return; - } - - // check that each datapoint is valid - for (int i = 0; i < dataPoints.size(); i++) // standard for-loop used instead of an enhanced for-loop to slightly reduce GC overhead due to iterator allocation - { - DhApiTerrainDataPoint dataPoint = dataPoints.get(i); - if (dataPoint == null) - { - throw new IllegalArgumentException("Null DhApiTerrainDataPoints are not allowed. If you want to represent empty terrain, please use AIR."); - } - - if (dataPoint.detailLevel != 0) - { - throw new IllegalArgumentException("DhApiTerrainDataPoints has the wrong detail level ["+dataPoint.detailLevel+"], all data points must be block sized; IE their detail level must be [0]."); - } + // we don't allow null columns + throw new IllegalArgumentException("Null columns aren't allowed. If you want to remove all data from a column please clear the list or pass in an empty list."); } @@ -150,16 +137,13 @@ public class DhApiChunk // set datapoints // //================// - // DH expects datapoints to be in a top-down order - DhApiTerrainDataPoint first = dataPoints.get(0); - DhApiTerrainDataPoint last = dataPoints.get(dataPoints.size() - 1); - if (first.bottomYBlockPos < last.bottomYBlockPos) + List column = this.dataPoints.get(internalArrayIndex); + if (column == null) { - // flip the array if it's in bottom-up order - Collections.reverse(dataPoints); + column = new ArrayList<>(); + this.dataPoints.set(internalArrayIndex, column); } - - this.dataPoints.set(internalArrayIndex, dataPoints); + column.addAll(dataPoints); } diff --git a/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java b/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java index 54ab66f10..78fcbaac3 100644 --- a/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java +++ b/api/src/main/java/com/seibel/distanthorizons/coreapi/ModInfo.java @@ -41,7 +41,7 @@ public final class ModInfo public static final String NAME = "DistantHorizons"; /** Human-readable version of NAME */ public static final String READABLE_NAME = "Distant Horizons"; - public static final String VERSION = "2.1.3-a-dev"; + public static final String VERSION = "2.2.1-a-dev"; /** Returns true if the current build is an unstable developer build, false otherwise. */ public static boolean IS_DEV_BUILD = VERSION.toLowerCase().contains("dev"); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java index 9fb3d53c7..d1ae1e7eb 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/api/internal/SharedApi.java @@ -147,23 +147,13 @@ public class SharedApi public static boolean isChunkAtBlockPosAlreadyUpdating(int blockPosX, int blockPosZ) { return UPDATING_CHUNK_POS_SET.contains(new DhChunkPos(new DhBlockPos2D(blockPosX, blockPosZ))); } + public static boolean isChunkAtChunkPosAlreadyUpdating(int chunkPosX, int chunkPosZ) + { return UPDATING_CHUNK_POS_SET.contains(new DhChunkPos(chunkPosX, chunkPosZ)); } + /** handles both block place and break events */ public void chunkBlockChangedEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, true); } - public void chunkLoadEvent(IChunkWrapper chunk, ILevelWrapper level) { this.applyChunkUpdate(chunk, level, false); } - public void chunkUnloadEvent(IChunkWrapper chunk, ILevelWrapper level) - { - // temporarily disabled since this was originally incorrectly designated as "chunkSaveEvent" - // but didn't actually fire on chunk save - // and generally this is unnecessary and drastically reduces LOD building performance - // when traveling around the world - if (false) - { - this.applyChunkUpdate(chunk, level, false); - } - } - public void applyChunkUpdate(IChunkWrapper chunkWrapper, ILevelWrapper level, boolean updateNeighborChunks) { diff --git a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java index 0cd1b2f12..0cb770a2a 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/dataObjects/transformers/LodDataBuilder.java @@ -19,6 +19,7 @@ package com.seibel.distanthorizons.core.dataObjects.transformers; +import java.util.Collections; import java.util.List; import com.seibel.distanthorizons.api.enums.config.EDhApiWorldCompressionMode; @@ -34,6 +35,7 @@ import com.seibel.distanthorizons.core.pos.DhBlockPos; import com.seibel.distanthorizons.core.pos.DhSectionPos; import com.seibel.distanthorizons.core.util.FullDataPointUtil; import com.seibel.distanthorizons.core.util.LodUtil; +import com.seibel.distanthorizons.core.util.RenderDataPointUtil; import com.seibel.distanthorizons.core.util.objects.DataCorruptedException; import com.seibel.distanthorizons.core.wrapperInterfaces.block.IBlockStateWrapper; import com.seibel.distanthorizons.core.wrapperInterfaces.chunk.IChunkWrapper; @@ -295,7 +297,7 @@ public class LodDataBuilder /** @throws ClassCastException if an API user returns the wrong object type(s) */ - public static FullDataSourceV2 createFromApiChunkData(DhApiChunk apiChunk) throws ClassCastException, DataCorruptedException + public static FullDataSourceV2 createFromApiChunkData(DhApiChunk apiChunk, boolean runAdditionalValidation) throws ClassCastException, DataCorruptedException, IllegalArgumentException { // get the section position int sectionPosX = getXOrZSectionPosFromChunkPos(apiChunk.chunkPosX); @@ -312,6 +314,10 @@ public class LodDataBuilder for (int relBlockX = 0; relBlockX < LodUtil.CHUNK_WIDTH; relBlockX++) { List columnDataPoints = apiChunk.getDataPoints(relBlockX, relBlockZ); + if (runAdditionalValidation) + { + validateOrThrowDataColumn(columnDataPoints); + } // this null check does 2 nice things at the same time: @@ -352,6 +358,75 @@ public class LodDataBuilder } return dataSource; } + private static void validateOrThrowDataColumn(List dataPoints) throws IllegalArgumentException + { + // order doesn't need to be checked if there is 0 or 1 items + if (dataPoints.size() > 1) + { + // DH expects datapoints to be in a top-down order + DhApiTerrainDataPoint first = dataPoints.get(0); + DhApiTerrainDataPoint last = dataPoints.get(dataPoints.size() - 1); + if (first.bottomYBlockPos < last.bottomYBlockPos) + { + // flip the array if it's in bottom-up order + Collections.reverse(dataPoints); + } + + } + + + + // check that each datapoint is valid + int lastBottomYPos = Integer.MIN_VALUE; + for (int i = 0; i < dataPoints.size(); i++) // standard for-loop used instead of an enhanced for-loop to slightly reduce GC overhead due to iterator allocation + { + DhApiTerrainDataPoint dataPoint = dataPoints.get(i); + + if (dataPoint == null) + { + throw new IllegalArgumentException("Datapoint: ["+i+"] is null DhApiTerrainDataPoints are not allowed. If you want to represent empty terrain, please use AIR."); + } + + if (dataPoint.detailLevel != 0) + { + throw new IllegalArgumentException("Datapoint: ["+i+"] has the wrong detail level ["+dataPoint.detailLevel+"], all data points must be block sized; IE their detail level must be [0]."); + } + + + + int bottomYPos = dataPoint.bottomYBlockPos; + int topYPos = dataPoint.topYBlockPos; + int height = (dataPoint.topYBlockPos - dataPoint.bottomYBlockPos); + + // is the datapoint right side up? + if (bottomYPos > topYPos) + { + throw new IllegalArgumentException("Datapoint: ["+i+"] is upside down. Top Pos: ["+topYPos+"], bottom pos: ["+bottomYPos+"]."); + } + // valid height? + if (height <= 0 || height >= RenderDataPointUtil.MAX_WORLD_Y_SIZE) + { + throw new IllegalArgumentException("Datapoint: ["+i+"] has invalid height. Height must be in the range [1 - "+RenderDataPointUtil.MAX_WORLD_Y_SIZE+"] (inclusive)."); + } + + // is this datapoint overlapping the last one? + if (lastBottomYPos > topYPos) + { + throw new IllegalArgumentException("DhApiTerrainDataPoint ["+i+"] is overlapping with the last datapoint, this top Y: ["+topYPos+"], lastBottomYPos: ["+lastBottomYPos+"]."); + } + // is there a gap between the last datapoint? + if (topYPos != lastBottomYPos + && lastBottomYPos != Integer.MIN_VALUE) + { + throw new IllegalArgumentException("DhApiTerrainDataPoint ["+i+"] has a gap between it and index ["+(i-1)+"]. Empty spaces should be filled by air, otherwise DH's downsampling won't calculate lighting correctly."); + } + + + lastBottomYPos = bottomYPos; + } + + } + diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/BatchGenerator.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/BatchGenerator.java index 598687bfe..655797a2a 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/BatchGenerator.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/BatchGenerator.java @@ -47,15 +47,6 @@ public class BatchGenerator implements IDhApiWorldGenerator private static final IWrapperFactory FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class); private static final Logger LOGGER = DhLoggerBuilder.getLogger(); - /** - * Defines how many tasks can be queued per thread.

- * - * TODO the multiplier here should change dynamically based on how fast the generator is vs the queuing thread, - * if this is too high it may cause issues when moving, - * but if it is too low the generator threads won't have enough tasks to work on - */ - private static final int MAX_QUEUED_TASKS_PER_THREAD = 3; - public AbstractBatchGenerationEnvironmentWrapper generationEnvironment; public IDhLevel targetDhLevel; @@ -147,14 +138,6 @@ public class BatchGenerator implements IDhApiWorldGenerator @Override public void preGeneratorTaskStart() { this.generationEnvironment.updateAllFutures(); } - @Override - public boolean isBusy() - { - int worldGenThreadCount = Math.max(Config.Client.Advanced.MultiThreading.numberOfWorldGenerationThreads.get(), 1); - int maxWorldGenTaskCount = worldGenThreadCount * MAX_QUEUED_TASKS_PER_THREAD; - return this.generationEnvironment.getEventCount() > maxWorldGenTaskCount; - } - //=========// diff --git a/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java b/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java index 64b8e9db3..5d72e8a2d 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/generation/WorldGenerationQueue.java @@ -56,6 +56,16 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb private static final Logger LOGGER = DhLoggerBuilder.getLogger(); private static final IWrapperFactory WRAPPER_FACTORY = SingletonInjector.INSTANCE.get(IWrapperFactory.class); + /** + * Defines how many tasks can be queued per thread.

+ * + * TODO the multiplier here should change dynamically based on how fast the generator is vs the queuing thread, + * if this is too high it may cause issues when moving, + * but if it is too low the generator threads won't have enough tasks to work on + */ + private static final int MAX_QUEUED_TASKS_PER_THREAD = 3; + + private final IDhApiWorldGenerator generator; /** contains the positions that need to be generated */ @@ -207,7 +217,7 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb // queue generation tasks until the generator is full, or there are no more tasks to generate boolean taskStarted = true; - while (!this.generator.isBusy() && taskStarted) + while (!this.isGeneratorBusy() && taskStarted) { taskStarted = this.startNextWorldGenTask(this.generationTargetPos); if (!taskStarted) @@ -234,6 +244,19 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb } }); } + public boolean isGeneratorBusy() + { + ThreadPoolExecutor executor = ThreadPoolUtil.getWorldGenExecutor(); + if (executor == null) + { + // shouldn't happen, but just in case, don't queue more tasks + return true; + } + + int worldGenThreadCount = Math.max(Config.Client.Advanced.MultiThreading.numberOfWorldGenerationThreads.get(), 1); + int maxWorldGenTaskCount = worldGenThreadCount * MAX_QUEUED_TASKS_PER_THREAD; + return executor.getQueue().size() > maxWorldGenTaskCount; + } /** * @param targetPos the position to center the generation around @@ -470,10 +493,10 @@ public class WorldGenerationQueue implements IFullDataSourceRetrievalQueue, IDeb { try { - FullDataSourceV2 dataSource = LodDataBuilder.createFromApiChunkData(dataPoints); + FullDataSourceV2 dataSource = LodDataBuilder.createFromApiChunkData(dataPoints, this.generator.runApiChunkValidation()); chunkDataConsumer.accept(dataSource); } - catch (DataCorruptedException e) + catch (DataCorruptedException | IllegalArgumentException e) { LOGGER.error("World generator returned a corrupt chunk. Error: [" + e.getMessage() + "]. World generator disabled.", e); Config.Client.Advanced.WorldGenerator.enableDistantGeneration.set(false); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogApplyShader.java b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogApplyShader.java index 4906b75b2..57a8aa5fb 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogApplyShader.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogApplyShader.java @@ -94,6 +94,12 @@ public class FogApplyShader extends AbstractShaderRenderer GL32.glBlendEquation(GL32.GL_FUNC_ADD); GL32.glBlendFuncSeparate(GL32.GL_SRC_ALPHA, GL32.GL_ONE_MINUS_SRC_ALPHA, GL32.GL_ONE, GL32.GL_ONE_MINUS_SRC_ALPHA); + // Depth testing must be disabled otherwise this application shader won't apply anything. + // setting this isn't necessary in vanilla, but some mods may change this, requiring it to be set manually, + // it should be automatically restored after rendering is complete. + GL32.glDisable(GL32.GL_DEPTH_TEST); + + // apply the rendered Fog to DH's framebuffer GL32.glBindFramebuffer(GL32.GL_READ_FRAMEBUFFER, FogShader.INSTANCE.frameBuffer); GL32.glBindFramebuffer(GL32.GL_DRAW_FRAMEBUFFER, LodRenderer.getActiveFramebufferId()); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogShader.java b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogShader.java index 5987af82e..f35eafc94 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogShader.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/FogShader.java @@ -167,6 +167,11 @@ public class FogShader extends AbstractShaderRenderer GL32.glBindTexture(GL32.GL_TEXTURE_2D, LodRenderer.getActiveDepthTextureId()); GL32.glUniform1i(this.uDepthMap, 0); + // this is necessary for MC 1.16 (IE Legacy OpenGL) + // otherwise the framebuffer isn't cleared correctly and the fog smears across the screen + GL32.glClear(GL32.GL_COLOR_BUFFER_BIT | GL32.GL_DEPTH_BUFFER_BIT); + + ScreenQuad.INSTANCE.render(); state.restore(); diff --git a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/SSAOApplyShader.java b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/SSAOApplyShader.java index 43da8521f..5dab1ac3a 100644 --- a/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/SSAOApplyShader.java +++ b/core/src/main/java/com/seibel/distanthorizons/core/render/renderer/shaders/SSAOApplyShader.java @@ -125,6 +125,12 @@ public class SSAOApplyShader extends AbstractShaderRenderer GL32.glBlendEquation(GL32.GL_FUNC_ADD); GL32.glBlendFuncSeparate(GL32.GL_ZERO, GL32.GL_SRC_ALPHA, GL32.GL_ZERO, GL32.GL_ONE); + // Depth testing must be disabled otherwise this application shader won't apply anything. + // setting this isn't necessary in vanilla, but some mods may change this, requiring it to be set manually, + // it should be automatically restored after rendering is complete. + GL32.glDisable(GL32.GL_DEPTH_TEST); + + // apply the rendered SSAO to the LODs GL32.glBindFramebuffer(GL32.GL_READ_FRAMEBUFFER, SSAOShader.INSTANCE.frameBuffer); GL32.glBindFramebuffer(GL32.GL_DRAW_FRAMEBUFFER, LodRenderer.getActiveFramebufferId());