From 3b475886efa6e57255795baf8a32fb09ce5e9a15 Mon Sep 17 00:00:00 2001 From: tom lee Date: Tue, 4 Jan 2022 15:10:41 +0800 Subject: [PATCH 01/20] Buffers: Fixed critical render bugs and vertex buffer leaks --- .../LodBufferBuilderFactory.java | 6 +- .../core/objects/opengl/LodVertexBuffer.java | 6 ++ .../seibel/lod/core/render/LodRenderer.java | 14 +-- .../seibel/lod/core/util/MovableGridList.java | 87 +++++++++++++++++++ 4 files changed, 106 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/seibel/lod/core/builders/bufferBuilding/LodBufferBuilderFactory.java b/src/main/java/com/seibel/lod/core/builders/bufferBuilding/LodBufferBuilderFactory.java index 100ffea61..c456beef8 100644 --- a/src/main/java/com/seibel/lod/core/builders/bufferBuilding/LodBufferBuilderFactory.java +++ b/src/main/java/com/seibel/lod/core/builders/bufferBuilding/LodBufferBuilderFactory.java @@ -280,7 +280,9 @@ public class LodBufferBuilderFactory vboY = buildableCenterBlockY; vboZ = buildableCenterBlockZ; buildableBuffers.move(playerRegionX, playerRegionZ); - buildableVbos.move(playerRegionX, playerRegionZ); + buildableVbos.move(playerRegionX, playerRegionZ, (bs) -> { + if (bs!=null) for (LodVertexBuffer b : bs) if (b!=null) b.close(); + }); setsToRender.move(playerRegionX, playerRegionZ); vertexOptimizerCache.move(playerRegionX, playerRegionZ); } @@ -847,7 +849,7 @@ public class LodBufferBuilderFactory drawableVbos = buildableVbos; buildableVbos = tmpVbo; - //ClientApi.LOGGER.info("Lod Swapped Buffers: "+drawableVbos.toDetailString()); + ClientApi.LOGGER.info("Lod Swapped Buffers: "+drawableVbos.toDetailString()); int tempX = drawableCenterBlockX; int tempY = drawableCenterBlockY; diff --git a/src/main/java/com/seibel/lod/core/objects/opengl/LodVertexBuffer.java b/src/main/java/com/seibel/lod/core/objects/opengl/LodVertexBuffer.java index 4bf6e94b5..77edef521 100644 --- a/src/main/java/com/seibel/lod/core/objects/opengl/LodVertexBuffer.java +++ b/src/main/java/com/seibel/lod/core/objects/opengl/LodVertexBuffer.java @@ -21,6 +21,7 @@ package com.seibel.lod.core.objects.opengl; import org.lwjgl.opengl.GL32; +import com.seibel.lod.core.api.ClientApi; import com.seibel.lod.core.enums.rendering.GLProxyContext; import com.seibel.lod.core.render.GLProxy; @@ -33,6 +34,7 @@ import com.seibel.lod.core.render.GLProxy; */ public class LodVertexBuffer implements AutoCloseable { + public static int count = 0; public int id; public int vertexCount; public final boolean isBufferStorage; @@ -44,6 +46,8 @@ public class LodVertexBuffer implements AutoCloseable throw new IllegalStateException("Thread [" +Thread.currentThread().getName() + "] tried to create a [" + LodVertexBuffer.class.getSimpleName() + "] outside a OpenGL contex."); this.id = GL32.glGenBuffers(); this.isBufferStorage = isBufferStorage; + count++; + ClientApi.LOGGER.info("LodVertexBuffer Count: "+count); } @@ -54,6 +58,8 @@ public class LodVertexBuffer implements AutoCloseable { GLProxy.getInstance().recordOpenGlCall(() -> GL32.glDeleteBuffers(this.id)); this.id = -1; + count--; + ClientApi.LOGGER.info("LodVertexBuffer Count: "+count); } } } \ No newline at end of file diff --git a/src/main/java/com/seibel/lod/core/render/LodRenderer.java b/src/main/java/com/seibel/lod/core/render/LodRenderer.java index 3d94d14a7..6e5957922 100644 --- a/src/main/java/com/seibel/lod/core/render/LodRenderer.java +++ b/src/main/java/com/seibel/lod/core/render/LodRenderer.java @@ -304,14 +304,18 @@ public class LodRenderer int lowRegionX = vbos.getCenterX() - vbos.gridCentreToEdge; int lowRegionZ = vbos.getCenterY() - vbos.gridCentreToEdge; int drawCall = 0; - for (int regionX=lowRegionX; regionX extends ArrayList implements List { super.add(null); } } + public void clear(Consumer d) { + super.forEach(d); + super.clear(); + super.ensureCapacity(gridSize*gridSize); + for (int i=0; i extends ArrayList implements List { centerY = newCenterY; return; } + centerX = newCenterX; + centerY = newCenterY; // X if (deltaX >= 0 && deltaY >= 0) @@ -136,8 +147,82 @@ public class MovableGridList extends ArrayList implements List { } } } + } + + public void move(int newCenterX, int newCenterY, Consumer d) { + if (centerX == newCenterX && centerY == newCenterY) return; + int deltaX = newCenterX - centerX; + int deltaY = newCenterY - centerY; + + // if the x or z offset is equal to or greater than + // the total width, just delete the current data + // and update the centerX and/or centerZ + if (Math.abs(deltaX) >= gridSize || Math.abs(deltaY) >= gridSize) + { + clear(d); + // update the new center + centerX = newCenterX; + centerY = newCenterY; + return; + } centerX = newCenterX; centerY = newCenterY; + + // Dealloc stuff + for (int x=0; x=gridSize || y-deltaY>=gridSize) { + d.accept(_getDirect(x,y)); + } + } + } + + // X + if (deltaX >= 0 && deltaY >= 0) + { + // move everything over to the left-up (as the center moves to the right-down) + for (int x = 0; x < gridSize; x++) + { + for (int y = 0; y < gridSize; y++) + { + _setDirect(x, y, _getDirect(x+deltaX, y+deltaY)); + } + } + } + else if (deltaX < 0 && deltaY >= 0) + { + // move everything over to the right-up (as the center moves to the left-down) + for (int x = gridSize - 1; x >= 0; x--) + { + for (int y = 0; y < gridSize; y++) + { + _setDirect(x, y, _getDirect(x+deltaX, y+deltaY)); + } + } + } + else if (deltaX >= 0 && deltaY < 0) + { + // move everything over to the left-down (as the center moves to the right-up) + for (int x = 0; x < gridSize; x++) + { + for (int y = gridSize - 1; y >= 0; y--) + { + _setDirect(x, y, _getDirect(x+deltaX, y+deltaY)); + } + } + } + else //if (deltaX < 0 && deltaY < 0) + { + // move everything over to the right-down (as the center moves to the left-up) + for (int x = gridSize - 1; x >= 0; x--) + { + for (int y = gridSize - 1; y >= 0; y--) + { + _setDirect(x, y, _getDirect(x+deltaX, y+deltaY)); + } + } + } } @@ -154,6 +239,8 @@ public class MovableGridList extends ArrayList implements List { public String toDetailString() { StringBuilder str = new StringBuilder("\n"); int i = 0; + str.append(toString()); + str.append("\n"); for (T t : this) { str.append(t!=null ? t.toString() : "NULL"); From 44bcc5ae01f3e4d5382c11cc7f5e5063db12b23d Mon Sep 17 00:00:00 2001 From: coolGi2007 Date: Tue, 4 Jan 2022 07:50:02 +0000 Subject: [PATCH 02/20] Added lower qulity clouds --- .../lod/textures/environment/clouds_small.png | Bin 0 -> 61739 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/main/resources/assets/lod/textures/environment/clouds_small.png diff --git a/src/main/resources/assets/lod/textures/environment/clouds_small.png b/src/main/resources/assets/lod/textures/environment/clouds_small.png new file mode 100644 index 0000000000000000000000000000000000000000..973b20e6e303558ab21afa9ef98875ad96c6e83d GIT binary patch literal 61739 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 z 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 Lrb4xxW$6C_xTnu` literal 0 HcmV?d00001 From 054988851de7df01453dc2f382eb7e05996a7bc6 Mon Sep 17 00:00:00 2001 From: tom lee Date: Tue, 4 Jan 2022 16:39:00 +0800 Subject: [PATCH 03/20] CutExpend: Fixed Cut bugs and some general fix to LodFileHandler stuff. --- .../LodBufferBuilderFactory.java | 2 +- .../handlers/LodDimensionFileHandler.java | 36 +++- .../lod/core/objects/lod/LodDimension.java | 186 +++++++++--------- .../lod/core/objects/lod/LodRegion.java | 12 +- 4 files changed, 126 insertions(+), 110 deletions(-) diff --git a/src/main/java/com/seibel/lod/core/builders/bufferBuilding/LodBufferBuilderFactory.java b/src/main/java/com/seibel/lod/core/builders/bufferBuilding/LodBufferBuilderFactory.java index c456beef8..f2756813c 100644 --- a/src/main/java/com/seibel/lod/core/builders/bufferBuilding/LodBufferBuilderFactory.java +++ b/src/main/java/com/seibel/lod/core/builders/bufferBuilding/LodBufferBuilderFactory.java @@ -849,7 +849,7 @@ public class LodBufferBuilderFactory drawableVbos = buildableVbos; buildableVbos = tmpVbo; - ClientApi.LOGGER.info("Lod Swapped Buffers: "+drawableVbos.toDetailString()); + //ClientApi.LOGGER.info("Lod Swapped Buffers: "+drawableVbos.toDetailString()); int tempX = drawableCenterBlockX; int tempY = drawableCenterBlockY; diff --git a/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHandler.java b/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHandler.java index 6473c6ade..53a4f8448 100644 --- a/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHandler.java +++ b/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHandler.java @@ -105,19 +105,35 @@ public class LodDimensionFileHandler //================// // read from file // //================// - + /** - * Returns the LodRegion at the given coordinates. + * Returns a new LodRegion at the given coordinates. * Returns an empty region if the file doesn't exist. */ public LodRegion loadRegionFromFile(byte detailLevel, RegionPos regionPos, DistanceGenerationMode generationMode, VerticalQuality verticalQuality) { - int regionX = regionPos.x; - int regionZ = regionPos.z; - LodRegion region = new LodRegion(LodUtil.REGION_DETAIL_LEVEL, regionPos, generationMode, verticalQuality); + LodRegion region = new LodRegion((byte) (LodUtil.REGION_DETAIL_LEVEL+1), regionPos, generationMode, verticalQuality); + return loadRegionFromFile(detailLevel, region, generationMode, verticalQuality); + } + + /** + * Returns the LodRegion that is filled at the given coordinates. + * Returns an empty region if the file doesn't exist. + */ + public LodRegion loadRegionFromFile(byte detailLevel, LodRegion region, DistanceGenerationMode generationMode, VerticalQuality verticalQuality) + { + if (region.getGenerationMode().compareTo(generationMode)<0 || region.getVerticalQuality().compareTo(verticalQuality)<0) { + //TODO: add flush and save region for old one + region = new LodRegion((byte) (LodUtil.REGION_DETAIL_LEVEL+1), region.getRegionPos(), generationMode, verticalQuality); + } + int regionX = region.regionPosX; + int regionZ = region.regionPosZ; - for (byte tempDetailLevel = LodUtil.REGION_DETAIL_LEVEL; tempDetailLevel >= detailLevel; tempDetailLevel--) + + + for (byte tempDetailLevel = (byte) (region.getMinDetailLevel()-1); tempDetailLevel >= detailLevel; tempDetailLevel--) { + File file = getBestMatchingRegionFile(tempDetailLevel, regionX, regionZ, generationMode, verticalQuality); if (file == null) { region.addLevelContainer(new VerticalLevelContainer(tempDetailLevel)); @@ -366,14 +382,14 @@ public class LodDimensionFileHandler // Return null if no file found private File getBestMatchingRegionFile(byte detailLevel, int regionX, int regionZ, DistanceGenerationMode targetGenMode, VerticalQuality targetVertQuality) { - DistanceGenerationMode genMode = targetGenMode; + DistanceGenerationMode genMode = DistanceGenerationMode.FULL; // Search from least GenMode to max GenMode, than least vertQuality to max vertQuality do { File file = getRegionFile(regionX, regionZ, genMode, detailLevel, targetVertQuality); if (file.exists()) return file; // Found target file. - targetGenMode = DistanceGenerationMode.next(targetGenMode); - if (targetGenMode == null) { // Failed to find any files for this vertQuality. Try next one up. - targetGenMode = genMode; + genMode = DistanceGenerationMode.previous(genMode); + if (genMode==null || genMode==DistanceGenerationMode.previous(targetGenMode)) { // Failed to find any files for this vertQuality. Try next one up. + genMode = DistanceGenerationMode.FULL; targetVertQuality = VerticalQuality.next(targetVertQuality); } } while (targetVertQuality != null); diff --git a/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java b/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java index 30d5c97c4..10a1acffa 100644 --- a/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java +++ b/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import com.seibel.lod.core.api.ClientApi; import com.seibel.lod.core.enums.config.DistanceGenerationMode; import com.seibel.lod.core.enums.config.GenerationPriority; import com.seibel.lod.core.enums.config.VerticalQuality; @@ -92,10 +93,9 @@ public class LodDimension private final RegionPos center; - /** prevents the cutAndExpandThread from expanding at the same location multiple times */ - private volatile AbstractChunkPosWrapper lastExpandedChunk; - /** prevents the cutAndExpandThread from cutting at the same location multiple times */ - private volatile AbstractChunkPosWrapper lastCutChunk; + private boolean isCutting = false; + private boolean isExpanding = false; + private final ExecutorService cutAndExpandThread = Executors.newSingleThreadExecutor(new LodThreadFactory(this.getClass().getSimpleName() + " - Cut and Expand")); /** @@ -104,8 +104,6 @@ public class LodDimension */ public LodDimension(IDimensionTypeWrapper newDimension, LodWorld lodWorld, int newWidth) { - lastCutChunk = null; - lastExpandedChunk = null; dimension = newDimension; width = newWidth; halfWidth = width / 2; @@ -159,6 +157,7 @@ public class LodDimension */ public synchronized void move(RegionPos regionOffset) { + ClientApi.LOGGER.info("LodDim MOVE. Offset: "+regionOffset); int xOffset = regionOffset.x; int zOffset = regionOffset.z; @@ -261,6 +260,7 @@ public class LodDimension // update the new center center.x += xOffset; center.z += zOffset; + ClientApi.LOGGER.info("LodDim MOVE complete. Offset: "+regionOffset); } @@ -362,109 +362,98 @@ public class LodDimension */ public void cutRegionNodesAsync(int playerPosX, int playerPosZ) { - AbstractChunkPosWrapper newPlayerChunk = FACTORY.createChunkPos(LevelPosUtil.getChunkPos((byte) 0, playerPosX), LevelPosUtil.getChunkPos((byte) 0, playerPosZ)); - - if (lastCutChunk == null) - lastCutChunk = FACTORY.createChunkPos(newPlayerChunk.getX() + 1, newPlayerChunk.getZ() - 1); - + if (isCutting) return; + isCutting = true; // don't run the tree cutter multiple times // for the same location - if (newPlayerChunk.getX() != lastCutChunk.getX() || newPlayerChunk.getZ() != lastCutChunk.getZ()) { - lastCutChunk = newPlayerChunk; + Runnable thread = () -> { + //ClientApi.LOGGER.info("LodDim cut Region: " + playerPosX + "," + playerPosZ); - Runnable thread = () -> { + // go over every region in the dimension + iterateWithSpiral((int x, int z) -> { + int regionX; + int regionZ; + int minDistance; + byte detail; + byte minAllowedDetailLevel; + regionX = (x + center.x) - halfWidth; + regionZ = (z + center.z) - halfWidth; - // go over every region in the dimension - iterateWithSpiral((int x, int z) -> { - int regionX; - int regionZ; - int minDistance; - byte detail; - byte minAllowedDetailLevel; - regionX = (x + center.x) - halfWidth; - regionZ = (z + center.z) - halfWidth; + if (regions[x][z] != null) { + // check what detail level this region should be + // and cut it if it is higher then that + minDistance = LevelPosUtil.minDistance(LodUtil.REGION_DETAIL_LEVEL, regionX, regionZ, + playerPosX, playerPosZ); + detail = DetailDistanceUtil.getTreeCutDetailFromDistance(minDistance); + minAllowedDetailLevel = DetailDistanceUtil.getCutLodDetail(detail); - if (regions[x][z] != null) { - // check what detail level this region should be - // and cut it if it is higher then that - minDistance = LevelPosUtil.minDistance(LodUtil.REGION_DETAIL_LEVEL, regionX, regionZ, - playerPosX, playerPosZ); - detail = DetailDistanceUtil.getTreeCutDetailFromDistance(minDistance); - minAllowedDetailLevel = DetailDistanceUtil.getCutLodDetail(detail); - - if (regions[x][z].getMinDetailLevel() > minAllowedDetailLevel) { - regions[x][z].cutTree(minAllowedDetailLevel); - regenRegionBuffer[x][z] = 2; - regenDimensionBuffers = true; - } + if (regions[x][z].getMinDetailLevel() < minAllowedDetailLevel) { + regions[x][z].cutTree(minAllowedDetailLevel); + regenRegionBuffer[x][z] = 2; + regenDimensionBuffers = true; } - }); - }; - cutAndExpandThread.execute(thread); - } + } + }); + //ClientApi.LOGGER.info("LodDim cut Region complete: " + playerPosX + "," + playerPosZ); + isCutting = false; + }; + cutAndExpandThread.execute(thread); } /** Either expands or loads all regions in the rendered LOD area */ public void expandOrLoadRegionsAsync(int playerPosX, int playerPosZ) { - DistanceGenerationMode generationMode = CONFIG.client().worldGenerator().getDistanceGenerationMode(); - AbstractChunkPosWrapper newPlayerChunk = FACTORY.createChunkPos(LevelPosUtil.getChunkPos((byte) 0, playerPosX), - LevelPosUtil.getChunkPos((byte) 0, playerPosZ)); - VerticalQuality verticalQuality = CONFIG.client().graphics().quality().getVerticalQuality(); - if (lastExpandedChunk == null) - lastExpandedChunk = FACTORY.createChunkPos(newPlayerChunk.getX() + 1, newPlayerChunk.getZ() - 1); + if (isExpanding) return; + isExpanding = true; + + DistanceGenerationMode generationMode = CONFIG.client().worldGenerator().getDistanceGenerationMode(); + VerticalQuality verticalQuality = CONFIG.client().graphics().quality().getVerticalQuality(); // don't run the expander multiple times // for the same location - if (newPlayerChunk.getX() != lastExpandedChunk.getX() || newPlayerChunk.getZ() != lastExpandedChunk.getZ()) { - lastExpandedChunk = newPlayerChunk; + Runnable thread = () -> { + //ClientApi.LOGGER.info("LodDim expend Region: " + playerPosX + "," + playerPosZ); - Runnable thread = () -> { + iterateWithSpiral((int x, int z) -> { + int regionX; + int regionZ; + LodRegion region; + int minDistance; + int maxDistance; + byte minDetail; + byte maxDetail; + regionX = (x + center.x) - halfWidth; + regionZ = (z + center.z) - halfWidth; + final RegionPos regionPos = new RegionPos(regionX, regionZ); + region = regions[x][z]; - iterateWithSpiral((int x, int z) -> { - int regionX; - int regionZ; - LodRegion region; - int minDistance; - byte detail; - byte levelToGen; - regionX = (x + center.x) - halfWidth; - regionZ = (z + center.z) - halfWidth; - final RegionPos regionPos = new RegionPos(regionX, regionZ); - region = regions[x][z]; + minDistance = LevelPosUtil.minDistance(LodUtil.REGION_DETAIL_LEVEL, regionX, regionZ, playerPosX, + playerPosZ); + maxDistance = LevelPosUtil.maxDistance(LodUtil.REGION_DETAIL_LEVEL, regionX, regionZ, playerPosX, + playerPosZ); + minDetail = DetailDistanceUtil.getTreeGenDetailFromDistance(minDistance); + maxDetail = DetailDistanceUtil.getTreeGenDetailFromDistance(maxDistance); + + boolean updated = false; + if (region == null) { + regions[x][z] = getRegionFromFile(regionPos, minDetail, generationMode, verticalQuality); + updated = true; + } else if (region.getGenerationMode().compareTo(generationMode) < 0 || + region.getVerticalQuality() != verticalQuality || + region.getMinDetailLevel() > minDetail) { + regions[x][z] = getRegionFromFile(regions[x][z], minDetail, generationMode, verticalQuality); + updated = true; + } + if (updated) { + regenRegionBuffer[x][z] = 2; + regenDimensionBuffers = true; + } + }); + //ClientApi.LOGGER.info("LodDim expend Region complete: " + playerPosX + "," + playerPosZ); + isExpanding = false; + }; - minDistance = LevelPosUtil.minDistance(LodUtil.REGION_DETAIL_LEVEL, regionX, regionZ, playerPosX, - playerPosZ); - detail = DetailDistanceUtil.getTreeGenDetailFromDistance(minDistance); - levelToGen = DetailDistanceUtil.getLodGenDetail(detail).detailLevel; - - // check that the region isn't null and at least this detail level - if (region == null || region.getGenerationMode() != generationMode) { - // First case, region has to be created - - // try to get the region from file - regions[x][z] = getRegionFromFile(regionPos, levelToGen, generationMode, verticalQuality); - - // if there is no region file create an empty region - if (regions[x][z] == null) - regions[x][z] = new LodRegion(levelToGen, regionPos, generationMode, verticalQuality); - - regenRegionBuffer[x][z] = 2; - regenDimensionBuffers = true; - } else if (region.getMinDetailLevel() > levelToGen) { - // Second case, the region exists at a higher detail level. - - // Expand the region by introducing the missing layer - region.growTree(levelToGen); - regions[x][z] = getRegionFromFile(regionPos, levelToGen, generationMode, verticalQuality); - regenRegionBuffer[x][z] = 2; - regenDimensionBuffers = true; - } - }); - }; - - cutAndExpandThread.execute(thread); - } + cutAndExpandThread.execute(thread); } /** @@ -846,7 +835,18 @@ public class LodDimension public LodRegion getRegionFromFile(RegionPos regionPos, byte detailLevel, DistanceGenerationMode generationMode, VerticalQuality verticalQuality) { - return fileHandler != null ? fileHandler.loadRegionFromFile(detailLevel, regionPos, generationMode, verticalQuality) : null; + return fileHandler != null ? fileHandler.loadRegionFromFile(detailLevel, regionPos, generationMode, verticalQuality) : + new LodRegion(detailLevel, regionPos, generationMode, verticalQuality); + } + /** + * Loads the region at the given region from file, + * if a file exists for that region. + */ + public LodRegion getRegionFromFile(LodRegion existingRegion, byte detailLevel, + DistanceGenerationMode generationMode, VerticalQuality verticalQuality) + { + return fileHandler != null ? fileHandler.loadRegionFromFile(detailLevel, existingRegion, generationMode, verticalQuality) : + new LodRegion(detailLevel, existingRegion.getRegionPos(), generationMode, verticalQuality); } /** Save all dirty regions in this LodDimension to file. */ diff --git a/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java b/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java index 02f98f2ba..dce0faee6 100644 --- a/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java +++ b/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java @@ -425,20 +425,20 @@ public class LodRegion int startX; int startZ; - // TODO what are each of these loops updating? - for (byte down = (byte) (minDetailLevel + 1); down <= detailLevel; down++) + // Update the level lower or equal to the detail level + for (byte down = (byte) (minDetailLevel+1); down <= detailLevel; down++) { startX = LevelPosUtil.convert(detailLevel, posX, down); startZ = LevelPosUtil.convert(detailLevel, posZ, down); width = 1 << (detailLevel - down); - for (int x = 0; x < width; x++) + for (int x = 0; x < width; x++) for (int z = 0; z < width; z++) update(down, startX + x, startZ + z); } - - - for (byte up = (byte) (detailLevel + 1); up <= LodUtil.REGION_DETAIL_LEVEL; up++) + + // Update the level higher than the detail level + for (byte up = (byte)(Math.max(detailLevel, minDetailLevel)+1); up <= LodUtil.REGION_DETAIL_LEVEL; up++) { update(up, LevelPosUtil.convert(detailLevel, posX, up), From f9871ef16dfd77ae6ce68f1e06ca055ed02d990f Mon Sep 17 00:00:00 2001 From: tom lee Date: Tue, 4 Jan 2022 18:57:25 +0800 Subject: [PATCH 04/20] WorldGen: Now no longer gen all chunks in higher than chunk details --- .../core/builders/lodBuilding/LodBuilder.java | 11 +- .../lod/core/objects/lod/LodDimension.java | 138 +++--------------- .../lod/core/objects/lod/LodRegion.java | 20 +-- .../core/objects/opengl/LodVertexBuffer.java | 4 +- 4 files changed, 40 insertions(+), 133 deletions(-) diff --git a/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java b/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java index fd29d5475..35c2da8a0 100644 --- a/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java +++ b/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java @@ -176,6 +176,7 @@ public class LodBuilder return; // determine how many LODs to generate horizontally + byte minDetailLevel = region.getMinDetailLevel(); HorizontalResolution detail = DetailDistanceUtil.getLodGenDetail(minDetailLevel); @@ -201,12 +202,14 @@ public class LodBuilder //lodDim.clear(detailLevel, posX, posZ); if (data != null && data.length != 0) { - posX = LevelPosUtil.convert((byte) 0, chunk.getChunkPosX() * 16 + startX, detail.detailLevel); - posZ = LevelPosUtil.convert((byte) 0, chunk.getChunkPosZ() * 16 + startZ, detail.detailLevel); - lodDim.addVerticalData(detailLevel, posX, posZ, data, false); + posX = LevelPosUtil.convert((byte) 0, chunk.getChunkPosX() * 16 + startX, minDetailLevel); + posZ = LevelPosUtil.convert((byte) 0, chunk.getChunkPosZ() * 16 + startZ, minDetailLevel); + if (!lodDim.doesDataExist(minDetailLevel, posX, posZ)) { + lodDim.addVerticalData(minDetailLevel, posX, posZ, data, false); + lodDim.updateData(minDetailLevel, posX, posZ); + } } } - lodDim.updateData(LodUtil.CHUNK_DETAIL_LEVEL, chunk.getChunkPosX(), chunk.getChunkPosZ()); //executeTime = System.currentTimeMillis() - executeTime; //if (executeTime > 0) ClientApi.LOGGER.info("generateLodNodeFromChunk level: " + detailLevel + " time ms: " + executeTime); } diff --git a/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java b/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java index 10a1acffa..e77fbd412 100644 --- a/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java +++ b/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java @@ -565,128 +565,36 @@ public class LodDimension dx = 0; dz = -1; - // We can use two type of generation scheduling - switch (CONFIG.client().worldGenerator().getGenerationPriority()) - { - case NEAR_FIRST: - //in the NEAR_FIRST generation scheduling we prioritize the nearest un-generated position to the player - //the chunk position to generate will be stored in a posToGenerate object - posToGenerate = new PosToGenerateContainer((byte) 10, maxDataToGenerate, playerBlockPosX, playerBlockPosZ); - - int playerChunkX = LevelPosUtil.getChunkPos(LodUtil.BLOCK_DETAIL_LEVEL, playerBlockPosX); - int playerChunkZ = LevelPosUtil.getChunkPos(LodUtil.BLOCK_DETAIL_LEVEL, playerBlockPosZ); - - int complexity; - int xChunkToCheck; - int zChunkToCheck; - byte detailLevel; - int posX; - int posZ; - long data; - int numbChunksWide = (width) * 32; - int circleLimit = Integer.MAX_VALUE; - - //posToGenerate is using an insertion sort algorithm which can become really fast if the - //original data order is almost ordered. For this reason we explore the matrix of the position to generate - //with a spiral matrix visit (a square spiral is almost ordered in the "nearest to farthest" order) - for (int i = 0; i < numbChunksWide * numbChunksWide; i++) - { - //Firstly we check if the posToGenerate has been filled - if (maxDataToGenerate == 0) - { - maxDataToGenerate--; - //if it has been filled then we set a stop distance - //the stop distance will be current distance (generically x) per square root of 2 - //this would guarantee a circular generation since (Math.abs(x) * 1.41f) is the - //radius of a circle that inscribe a square - circleLimit = (int) (Math.abs(x) * 1.41f); - } - //This second if check if we reached the circleLimit decided in the previous if - //if so we stop - else if (maxDataToGenerate < 0) - { - if (circleLimit < Math.abs(x) && circleLimit < Math.abs(z)) - break; - } - - - xChunkToCheck = x + playerChunkX; - zChunkToCheck = z + playerChunkZ; - - //we get the lod region in which the chunk is present - lodRegion = getRegion(LodUtil.CHUNK_DETAIL_LEVEL, xChunkToCheck, zChunkToCheck); - if (lodRegion == null) - continue; - - //Now we check if the current chunk has been generated with the correct complexity - //if(lodRegion.isChunkPreGenerated(xChunkToCheck,zChunkToCheck)) - // complexity = DistanceGenerationMode.SERVER.complexity; - //else - complexity = CONFIG.client().worldGenerator().getDistanceGenerationMode().complexity; - - - //we create the level position info of the chunk - detailLevel = lodRegion.getMinDetailLevel(); - posX = LevelPosUtil.convert(LodUtil.CHUNK_DETAIL_LEVEL, xChunkToCheck, detailLevel); - posZ = LevelPosUtil.convert(LodUtil.CHUNK_DETAIL_LEVEL, zChunkToCheck, detailLevel); - - data = getSingleData(detailLevel, posX, posZ); - - //we will generate the position only if the current generation complexity is lower than the target one. - //an un-generated area will always have 0 generation - if (DataPointUtil.getGenerationMode(data) < complexity) - { - posToGenerate.addPosToGenerate(detailLevel, posX, posZ); - if (maxDataToGenerate >= 0) - maxDataToGenerate--; - } - - //with this code section we find the next chunk to check - if ((x == z) || ((x < 0) && (x == -z)) || ((x > 0) && (x == 1 - z))) - { - t = dx; - dx = -dz; - dz = t; - } - x += dx; - z += dz; - } - break; + //in the FAR_FIRST generation we dedicate part of the generation process to the far region with really + //low detail quality. - default: - case FAR_FIRST: - //in the FAR_FIRST generation we dedicate part of the generation process to the far region with really - //low detail quality. + posToGenerate = new PosToGenerateContainer((byte) 8, maxDataToGenerate, playerBlockPosX, playerBlockPosZ); + + int xRegion; + int zRegion; + + for (int i = 0; i < width * width; i++) + { + xRegion = x + center.x; + zRegion = z + center.z; - posToGenerate = new PosToGenerateContainer((byte) 8, maxDataToGenerate, playerBlockPosX, playerBlockPosZ); + //All of this is handled directly by the region, which scan every pos from top to bottom of the quad tree + lodRegion = getRegion(xRegion, zRegion); + if (lodRegion != null) + lodRegion.getPosToGenerate(posToGenerate, playerBlockPosX, playerBlockPosZ); - int xRegion; - int zRegion; - for (int i = 0; i < width * width; i++) + //with this code section we find the next chunk to check + if ((x == z) || ((x < 0) && (x == -z)) || ((x > 0) && (x == 1 - z))) { - xRegion = x + center.x; - zRegion = z + center.z; - - //All of this is handled directly by the region, which scan every pos from top to bottom of the quad tree - lodRegion = getRegion(xRegion, zRegion); - if (lodRegion != null) - lodRegion.getPosToGenerate(posToGenerate, playerBlockPosX, playerBlockPosZ); - - - //with this code section we find the next chunk to check - if ((x == z) || ((x < 0) && (x == -z)) || ((x > 0) && (x == 1 - z))) - { - t = dx; - dx = -dz; - dz = t; - } - x += dx; - z += dz; + t = dx; + dx = -dz; + dz = t; } - break; + x += dx; + z += dz; } - return posToGenerate; + return posToGenerate; } /** diff --git a/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java b/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java index dce0faee6..5c48ba045 100644 --- a/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java +++ b/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java @@ -162,7 +162,7 @@ public class LodRegion // The dataContainer could have null entries if the // detailLevel changes. if (this.dataContainer[detailLevel] == null) - this.dataContainer[detailLevel] = new VerticalLevelContainer(detailLevel); + return false;//this.dataContainer[detailLevel] = new VerticalLevelContainer(detailLevel); this.dataContainer[detailLevel].addData(data, posX, posZ, verticalIndex); @@ -183,7 +183,7 @@ public class LodRegion // The dataContainer could have null entries if the // detailLevel changes. if (this.dataContainer[detailLevel] == null) - this.dataContainer[detailLevel] = new VerticalLevelContainer(detailLevel); + return false;//this.dataContainer[detailLevel] = new VerticalLevelContainer(detailLevel); return this.dataContainer[detailLevel].addVerticalData(data, posX, posZ); } @@ -248,7 +248,7 @@ public class LodRegion int size = 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel); // calculate what LevelPos are in range to generate - int maxDistance = LevelPosUtil.maxDistance(detailLevel, childOffsetPosX, childOffsetPosZ, playerPosX, playerPosZ, regionPosX, regionPosZ); + int minDistance = LevelPosUtil.minDistance(detailLevel, childOffsetPosX, childOffsetPosZ, playerPosX, playerPosZ, regionPosX, regionPosZ); // determine this child's levelPos byte childDetailLevel = (byte) (detailLevel - 1); @@ -257,7 +257,7 @@ public class LodRegion int childSize = 1 << (LodUtil.REGION_DETAIL_LEVEL - childDetailLevel); - byte targetDetailLevel = DetailDistanceUtil.getLodGenDetail(DetailDistanceUtil.getGenerationDetailFromDistance(maxDistance)).detailLevel; + byte targetDetailLevel = DetailDistanceUtil.getGenerationDetailFromDistance(minDistance); if (targetDetailLevel <= detailLevel) { if (targetDetailLevel == detailLevel) @@ -299,14 +299,10 @@ public class LodRegion { // The detail Level is smaller than a chunk. // Only recurse down the top right child. - - if (DetailDistanceUtil.getLodGenDetail(childDetailLevel).detailLevel <= (childDetailLevel)) - { - if (!doesDataExist(childDetailLevel, childPosX, childPosZ)) - posToGenerate.addPosToGenerate(childDetailLevel, childPosX + regionPosX * childSize, childPosZ + regionPosZ * childSize); - else - getPosToGenerate(posToGenerate, childDetailLevel, childPosX, childPosZ, playerPosX, playerPosZ); - } + if (!doesDataExist(childDetailLevel, childPosX, childPosZ)) + posToGenerate.addPosToGenerate(childDetailLevel, childPosX + regionPosX * childSize, childPosZ + regionPosZ * childSize); + else + getPosToGenerate(posToGenerate, childDetailLevel, childPosX, childPosZ, playerPosX, playerPosZ); } } } diff --git a/src/main/java/com/seibel/lod/core/objects/opengl/LodVertexBuffer.java b/src/main/java/com/seibel/lod/core/objects/opengl/LodVertexBuffer.java index 77edef521..a0ed97a48 100644 --- a/src/main/java/com/seibel/lod/core/objects/opengl/LodVertexBuffer.java +++ b/src/main/java/com/seibel/lod/core/objects/opengl/LodVertexBuffer.java @@ -47,7 +47,7 @@ public class LodVertexBuffer implements AutoCloseable this.id = GL32.glGenBuffers(); this.isBufferStorage = isBufferStorage; count++; - ClientApi.LOGGER.info("LodVertexBuffer Count: "+count); + //ClientApi.LOGGER.info("LodVertexBuffer Count: "+count); } @@ -59,7 +59,7 @@ public class LodVertexBuffer implements AutoCloseable GLProxy.getInstance().recordOpenGlCall(() -> GL32.glDeleteBuffers(this.id)); this.id = -1; count--; - ClientApi.LOGGER.info("LodVertexBuffer Count: "+count); + //ClientApi.LOGGER.info("LodVertexBuffer Count: "+count); } } } \ No newline at end of file From 4bf004ae6b49bc1afe33c9b1abafa1d93a0988e5 Mon Sep 17 00:00:00 2001 From: James Seibel Date: Wed, 5 Jan 2022 05:26:39 +0000 Subject: [PATCH 05/20] Add class_diagram.drawio --- _wiki-files/class_diagram.drawio | 1 + 1 file changed, 1 insertion(+) create mode 100644 _wiki-files/class_diagram.drawio diff --git a/_wiki-files/class_diagram.drawio b/_wiki-files/class_diagram.drawio new file mode 100644 index 000000000..59bbb0fd0 --- /dev/null +++ b/_wiki-files/class_diagram.drawio @@ -0,0 +1 @@ +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 From d9dc33a10588d399b1b22d4af074d3ca8b0e6e37 Mon Sep 17 00:00:00 2001 From: cola98765 Date: Wed, 5 Jan 2022 10:53:56 +0100 Subject: [PATCH 06/20] added gen mode debug view --- .../builders/bufferBuilding/CubicLodTemplate.java | 7 ++++++- .../seibel/lod/core/enums/rendering/DebugMode.java | 12 ++++++++++-- .../java/com/seibel/lod/core/render/LodRenderer.java | 2 +- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/seibel/lod/core/builders/bufferBuilding/CubicLodTemplate.java b/src/main/java/com/seibel/lod/core/builders/bufferBuilding/CubicLodTemplate.java index a12229f84..76730bc06 100644 --- a/src/main/java/com/seibel/lod/core/builders/bufferBuilding/CubicLodTemplate.java +++ b/src/main/java/com/seibel/lod/core/builders/bufferBuilding/CubicLodTemplate.java @@ -54,7 +54,12 @@ public class CubicLodTemplate int color; if (debugging != DebugMode.OFF) - color = LodUtil.DEBUG_DETAIL_LEVEL_COLORS[detailLevel].getRGB(); + { + if (debugging == DebugMode.SHOW_DETAIL || debugging == DebugMode.SHOW_DETAIL_WIREFRAME) + color = LodUtil.DEBUG_DETAIL_LEVEL_COLORS[detailLevel].getRGB(); + else ///if (debugging == DebugMode.SHOW_GENMODE || debugging == DebugMode.SHOW_GENMODE_WIREFRAME) + color = LodUtil.DEBUG_DETAIL_LEVEL_COLORS[DataPointUtil.getGenerationMode(data)].getRGB(); + } else color = DataPointUtil.getColor(data); diff --git a/src/main/java/com/seibel/lod/core/enums/rendering/DebugMode.java b/src/main/java/com/seibel/lod/core/enums/rendering/DebugMode.java index cdc02fa1f..90baabd48 100644 --- a/src/main/java/com/seibel/lod/core/enums/rendering/DebugMode.java +++ b/src/main/java/com/seibel/lod/core/enums/rendering/DebugMode.java @@ -34,7 +34,13 @@ public enum DebugMode SHOW_DETAIL, /** LOD colors are based on their detail, and draws in wireframe. */ - SHOW_DETAIL_WIREFRAME; + SHOW_DETAIL_WIREFRAME, + + /** LOD colors are based on their gen mode. */ + SHOW_GENMODE, + + /** LOD colors are based on their gen mode, and draws in wireframe. */ + SHOW_GENMODE_WIREFRAME; /** used when cycling through the different modes */ private DebugMode next; @@ -43,7 +49,9 @@ public enum DebugMode { OFF.next = SHOW_DETAIL; SHOW_DETAIL.next = SHOW_DETAIL_WIREFRAME; - SHOW_DETAIL_WIREFRAME.next = OFF; + SHOW_DETAIL_WIREFRAME.next = SHOW_GENMODE; + SHOW_GENMODE.next = SHOW_GENMODE_WIREFRAME; + SHOW_GENMODE_WIREFRAME.next = OFF; } /** returns the next debug mode */ diff --git a/src/main/java/com/seibel/lod/core/render/LodRenderer.java b/src/main/java/com/seibel/lod/core/render/LodRenderer.java index 6e5957922..47d6b0f22 100644 --- a/src/main/java/com/seibel/lod/core/render/LodRenderer.java +++ b/src/main/java/com/seibel/lod/core/render/LodRenderer.java @@ -245,7 +245,7 @@ public class LodRenderer GL32.glBindBuffer(GL32.GL_ARRAY_BUFFER, 0); // set the required open GL settings - if (CONFIG.client().advanced().debugging().getDebugMode() == DebugMode.SHOW_DETAIL_WIREFRAME) + if (CONFIG.client().advanced().debugging().getDebugMode() == DebugMode.SHOW_DETAIL_WIREFRAME || CONFIG.client().advanced().debugging().getDebugMode() == DebugMode.SHOW_GENMODE_WIREFRAME) GL32.glPolygonMode(GL32.GL_FRONT_AND_BACK, GL32.GL_LINE); else GL32.glPolygonMode(GL32.GL_FRONT_AND_BACK, GL32.GL_FILL); From e71660eb4153cd27e070600b446eca6771b70369 Mon Sep 17 00:00:00 2001 From: tom lee Date: Wed, 5 Jan 2022 19:17:31 +0800 Subject: [PATCH 07/20] Fixed generation issues and buffer now update more aggresively --- .../worldGeneration/LodWorldGenerator.java | 9 +++- .../lod/core/objects/lod/LodDimension.java | 44 ++++--------------- .../lod/core/objects/lod/LodRegion.java | 31 ++----------- 3 files changed, 21 insertions(+), 63 deletions(-) diff --git a/src/main/java/com/seibel/lod/core/builders/worldGeneration/LodWorldGenerator.java b/src/main/java/com/seibel/lod/core/builders/worldGeneration/LodWorldGenerator.java index cddd755f6..4baa4bc30 100644 --- a/src/main/java/com/seibel/lod/core/builders/worldGeneration/LodWorldGenerator.java +++ b/src/main/java/com/seibel/lod/core/builders/worldGeneration/LodWorldGenerator.java @@ -28,6 +28,7 @@ import java.util.concurrent.atomic.AtomicInteger; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.seibel.lod.core.builders.lodBuilding.LodBuilder; import com.seibel.lod.core.enums.config.DistanceGenerationMode; +import com.seibel.lod.core.enums.config.GenerationPriority; import com.seibel.lod.core.objects.PosToGenerateContainer; import com.seibel.lod.core.objects.lod.LodDimension; import com.seibel.lod.core.util.LevelPosUtil; @@ -115,6 +116,10 @@ public class LodWorldGenerator // This is fine currently since DistanceGenerationMode doesn't care about the detail level for now. // However, If that was to be changed, This will need to be fixed. DistanceGenerationMode mode = CONFIG.client().worldGenerator().getDistanceGenerationMode(); + final GenerationPriority priority; + if (CONFIG.client().worldGenerator().getGenerationPriority() == GenerationPriority.AUTO) + priority = MC.hasSinglePlayerServer() ? GenerationPriority.FAR_FIRST : GenerationPriority.NEAR_FIRST; + else priority = CONFIG.client().worldGenerator().getGenerationPriority(); if (mode != DistanceGenerationMode.NONE && !generatorThreadRunning @@ -192,7 +197,9 @@ public class LodWorldGenerator // add the far positions - if (farIndex < posToGenerate.getNumberOfFarPos() && posToGenerate.getNthDetail(farIndex, false) != 0) + // But if priority is NEAR_FIRST, we only do that if near pos has ran out. + if ((nearIndex >= posToGenerate.getNumberOfNearPos() || priority != GenerationPriority.NEAR_FIRST) && + farIndex < posToGenerate.getNumberOfFarPos() && posToGenerate.getNthDetail(farIndex, false) != 0) { detailLevel = (byte) (posToGenerate.getNthDetail(farIndex, false) - 1); posX = posToGenerate.getNthPosX(farIndex, false); diff --git a/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java b/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java index e77fbd412..8c5aa7f2c 100644 --- a/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java +++ b/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java @@ -443,7 +443,13 @@ public class LodDimension region.getMinDetailLevel() > minDetail) { regions[x][z] = getRegionFromFile(regions[x][z], minDetail, generationMode, verticalQuality); updated = true; + } else if (region.lastMaxDetailLevel != maxDetail) { + region.lastMaxDetailLevel = maxDetail; + updated = true; + } else if (region.lastMaxDetailLevel != region.getMinDetailLevel()) { + updated = true; } + if (updated) { regenRegionBuffer[x][z] = 2; regenDimensionBuffers = true; @@ -555,45 +561,13 @@ public class LodDimension public PosToGenerateContainer getPosToGenerate(int maxDataToGenerate, int playerBlockPosX, int playerBlockPosZ) { PosToGenerateContainer posToGenerate; - LodRegion lodRegion; - // all the following values are used for the spiral matrix visit - // x and z are the matrix coord - // dx and dz is the next move on the coordinate in the range -1 0 +1 - int x, z, dx, dz, t; - x = 0; - z = 0; - dx = 0; - dz = -1; - - //in the FAR_FIRST generation we dedicate part of the generation process to the far region with really - //low detail quality. - posToGenerate = new PosToGenerateContainer((byte) 8, maxDataToGenerate, playerBlockPosX, playerBlockPosZ); - - int xRegion; - int zRegion; - - for (int i = 0; i < width * width; i++) - { - xRegion = x + center.x; - zRegion = z + center.z; - + iterateWithSpiral((int x, int z) -> { //All of this is handled directly by the region, which scan every pos from top to bottom of the quad tree - lodRegion = getRegion(xRegion, zRegion); + LodRegion lodRegion = regions[x][z]; if (lodRegion != null) lodRegion.getPosToGenerate(posToGenerate, playerBlockPosX, playerBlockPosZ); - - - //with this code section we find the next chunk to check - if ((x == z) || ((x < 0) && (x == -z)) || ((x > 0) && (x == 1 - z))) - { - t = dx; - dx = -dz; - dz = t; - } - x += dx; - z += dz; - } + }); return posToGenerate; } diff --git a/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java b/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java index 5c48ba045..304444c1d 100644 --- a/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java +++ b/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java @@ -47,6 +47,7 @@ public class LodRegion /** Holds the lowest (least detailed) detail level in this region */ private byte minDetailLevel; + public byte lastMaxDetailLevel = LodUtil.REGION_DETAIL_LEVEL; /** * This holds all data for this region @@ -75,7 +76,6 @@ public class LodRegion this.generationMode = generationMode; dataContainer = new LevelContainer[POSSIBLE_LOD]; - // Initialize all the different matrices for (byte lod = minDetailLevel; lod <= LodUtil.REGION_DETAIL_LEVEL; lod++) { @@ -248,7 +248,7 @@ public class LodRegion int size = 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel); // calculate what LevelPos are in range to generate - int minDistance = LevelPosUtil.minDistance(detailLevel, childOffsetPosX, childOffsetPosZ, playerPosX, playerPosZ, regionPosX, regionPosZ); + int minDistance = LevelPosUtil.minDistance(LodUtil.REGION_DETAIL_LEVEL, regionPosX, regionPosZ, playerPosX, playerPosZ); // determine this child's levelPos byte childDetailLevel = (byte) (detailLevel - 1); @@ -273,36 +273,13 @@ public class LodRegion if (detailLevel > LodUtil.CHUNK_DETAIL_LEVEL) { - int ungeneratedChildren = 0; - - // make sure all children are generated to this detailLevel for (int x = 0; x <= 1; x++) - { for (int z = 0; z <= 1; z++) - { - if (!doesDataExist(childDetailLevel, childPosX + x, childPosZ + z)) - { - ungeneratedChildren++; - posToGenerate.addPosToGenerate(childDetailLevel, childPosX + x + regionPosX * childSize, childPosZ + z + regionPosZ * childSize); - } - } - } - - // only if all the children are correctly generated - // should we go deeper - if (ungeneratedChildren == 0) - for (int x = 0; x <= 1; x++) - for (int z = 0; z <= 1; z++) - getPosToGenerate(posToGenerate, childDetailLevel, childPosX + x, childPosZ + z, playerPosX, playerPosZ); + getPosToGenerate(posToGenerate, childDetailLevel, childPosX + x, childPosZ + z, playerPosX, playerPosZ); } else { - // The detail Level is smaller than a chunk. - // Only recurse down the top right child. - if (!doesDataExist(childDetailLevel, childPosX, childPosZ)) - posToGenerate.addPosToGenerate(childDetailLevel, childPosX + regionPosX * childSize, childPosZ + regionPosZ * childSize); - else - getPosToGenerate(posToGenerate, childDetailLevel, childPosX, childPosZ, playerPosX, playerPosZ); + getPosToGenerate(posToGenerate, childDetailLevel, childPosX, childPosZ, playerPosX, playerPosZ); } } } From 1a5fd873463bb56af1fe8e93fe1081109f23cb8b Mon Sep 17 00:00:00 2001 From: tom lee Date: Thu, 6 Jan 2022 00:07:19 +0800 Subject: [PATCH 08/20] Fixed saving. Guarrantee save is complete on exit. Trashed some logging Also, tried (keyword: tried) fixing the FAR_FIRST generation. --- .../com/seibel/lod/core/api/EventApi.java | 18 +- .../LodBufferBuilderFactory.java | 8 +- .../core/builders/lodBuilding/LodBuilder.java | 2 +- .../worldGeneration/LodWorldGenerator.java | 17 +- .../handlers/LodDimensionFileHandler.java | 118 ++++++++--- .../core/objects/PosToGenerateContainer.java | 2 +- .../lod/core/objects/lod/LodDimension.java | 194 +++++------------- .../lod/core/objects/lod/LodRegion.java | 88 ++------ .../seibel/lod/core/objects/lod/LodWorld.java | 14 +- .../lod/core/objects/lod/RegionPos.java | 19 ++ .../seibel/lod/core/render/LodRenderer.java | 2 +- 11 files changed, 207 insertions(+), 275 deletions(-) diff --git a/src/main/java/com/seibel/lod/core/api/EventApi.java b/src/main/java/com/seibel/lod/core/api/EventApi.java index 0930c48ac..429252e82 100644 --- a/src/main/java/com/seibel/lod/core/api/EventApi.java +++ b/src/main/java/com/seibel/lod/core/api/EventApi.java @@ -81,7 +81,6 @@ public class EventApi if (lodDim == null) return; - // FIXME: This is in server thread. We shouldn't be accessing the client's renderer! LodWorldGenerator.INSTANCE.queueGenerationRequests(lodDim, ApiShared.lodBuilder); } @@ -99,7 +98,7 @@ public class EventApi public void worldSaveEvent() { - ApiShared.lodWorld.saveAllDimensions(); + ApiShared.lodWorld.saveAllDimensions(false); // Do an async save. } /** This is also called when a new dimension loads */ @@ -126,11 +125,12 @@ public class EventApi ThreadMapUtil.clearMaps(); // ClientApi.renderer.markForCleanup(); // ClientApi.renderer.destroyBuffers(); - - new Thread(() -> checkIfDisconnectedFromServer()).start(); + checkIfDisconnectedFromServer(); + //new Thread(() -> checkIfDisconnectedFromServer()).start(); } private void checkIfDisconnectedFromServer() { + /* try { // world unloading events are called before disconnecting from the server, @@ -141,10 +141,10 @@ public class EventApi { // this should never happen, but just in case e.printStackTrace(); - } + }*/ - if (MC.getWrappedClientWorld() == null || (!MC.connectedToServer() && !MC.hasSinglePlayerServer())) + //if (MC.getWrappedClientWorld() == null || (!MC.connectedToServer() && !MC.hasSinglePlayerServer())) { // the player just left the server @@ -157,7 +157,6 @@ public class EventApi LodWorldGenerator.INSTANCE.numberOfChunksWaitingToGenerate.set(0); ApiShared.lodWorld.deselectWorld(); - // prevent issues related to the buffer builder // breaking or retaining previous data when changing worlds. ClientApi.renderer.destroyBuffers(); @@ -213,7 +212,6 @@ public class EventApi RegionPos worldRegionOffset = new RegionPos(playerRegionPos.x - lodDim.getCenterRegionPosX(), playerRegionPos.z - lodDim.getCenterRegionPosZ()); if (worldRegionOffset.x != 0 || worldRegionOffset.z != 0) { - ApiShared.lodWorld.saveAllDimensions(); lodDim.move(worldRegionOffset); //LOGGER.info("offset: " + worldRegionOffset.x + "," + worldRegionOffset.z + "\t center: " + lodDim.getCenterX() + "," + lodDim.getCenterZ()); } @@ -236,12 +234,10 @@ public class EventApi // do the dimensions need to change in size? if (ApiShared.lodBuilder.defaultDimensionWidthInRegions != newWidth || recalculateWidths) { - ApiShared.lodWorld.saveAllDimensions(); - // update the dimensions to fit the new width ApiShared.lodWorld.resizeDimensionRegionWidth(newWidth); ApiShared.lodBuilder.defaultDimensionWidthInRegions = newWidth; - ClientApi.renderer.setupBuffers(ApiShared.lodWorld.getLodDimension(MC.getCurrentDimension())); + ClientApi.renderer.setupBuffers(); recalculateWidths = false; //LOGGER.info("new dimension width in regions: " + newWidth + "\t potential: " + newWidth ); diff --git a/src/main/java/com/seibel/lod/core/builders/bufferBuilding/LodBufferBuilderFactory.java b/src/main/java/com/seibel/lod/core/builders/bufferBuilding/LodBufferBuilderFactory.java index f2756813c..bca3a0bbe 100644 --- a/src/main/java/com/seibel/lod/core/builders/bufferBuilding/LodBufferBuilderFactory.java +++ b/src/main/java/com/seibel/lod/core/builders/bufferBuilding/LodBufferBuilderFactory.java @@ -78,7 +78,7 @@ public class LodBufferBuilderFactory public void end(String source) { timer = System.nanoTime() - timer; if (timer> 16000000) { //16 ms - ClientApi.LOGGER.info("NOTE: "+source+" took "+Duration.ofNanos(timer)+"!"); + ClientApi.LOGGER.debug("NOTE: "+source+" took "+Duration.ofNanos(timer)+"!"); } } @@ -372,7 +372,7 @@ public class LodBufferBuilderFactory long buildTime = endTime - startTime; long executeTime = executeEnd - executeStart; if (enableLogging) - ClientApi.LOGGER.info("Thread Build("+nodeToRenderThreads.size()+"/"+(lodDim.getWidth()*lodDim.getWidth())+ (fullRegen ? "FULL" : "")+") time: " + buildTime + " ms" + '\n' + + ClientApi.LOGGER.debug("Thread Build("+nodeToRenderThreads.size()+"/"+(lodDim.getWidth()*lodDim.getWidth())+ (fullRegen ? "FULL" : "")+") time: " + buildTime + " ms" + '\n' + "thread execute time: " + executeTime + " ms"); //================================// @@ -400,7 +400,7 @@ public class LodBufferBuilderFactory uploadBuffers(posToUpload); long uploadTime = System.currentTimeMillis() - startUploadTime; if (enableLogging) - ClientApi.LOGGER.info("Thread Upload time: " + uploadTime + " ms"); + ClientApi.LOGGER.debug("Thread Upload time: " + uploadTime + " ms"); } catch (Exception e) { @@ -840,7 +840,7 @@ public class LodBufferBuilderFactory private boolean swapBuffers() { bufferLock.lock(); - ClientApi.LOGGER.info("Lod Swap Buffers"); + ClientApi.LOGGER.debug("Lod Swap Buffers"); { boolean shouldRegenBuff = true; try diff --git a/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java b/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java index 35c2da8a0..d26c86ad0 100644 --- a/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java +++ b/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java @@ -205,7 +205,7 @@ public class LodBuilder posX = LevelPosUtil.convert((byte) 0, chunk.getChunkPosX() * 16 + startX, minDetailLevel); posZ = LevelPosUtil.convert((byte) 0, chunk.getChunkPosZ() * 16 + startZ, minDetailLevel); if (!lodDim.doesDataExist(minDetailLevel, posX, posZ)) { - lodDim.addVerticalData(minDetailLevel, posX, posZ, data, false); + lodDim.addVerticalData(minDetailLevel, posX, posZ, data); lodDim.updateData(minDetailLevel, posX, posZ); } } diff --git a/src/main/java/com/seibel/lod/core/builders/worldGeneration/LodWorldGenerator.java b/src/main/java/com/seibel/lod/core/builders/worldGeneration/LodWorldGenerator.java index 4baa4bc30..28321ee87 100644 --- a/src/main/java/com/seibel/lod/core/builders/worldGeneration/LodWorldGenerator.java +++ b/src/main/java/com/seibel/lod/core/builders/worldGeneration/LodWorldGenerator.java @@ -97,12 +97,21 @@ public class LodWorldGenerator // TODO: Rename the config option if (CONFIG.client().worldGenerator().getAllowUnstableFeatureGeneration()) { if (experimentalWorldGenerator == null) { - experimentalWorldGenerator = WRAPPER_FACTORY.createExperimentalWorldGenerator(lodBuilder, lodDim, world); + try { + experimentalWorldGenerator = WRAPPER_FACTORY.createExperimentalWorldGenerator(lodBuilder, lodDim, world); if (experimentalWorldGenerator == null) CONFIG.client().worldGenerator().setAllowUnstableFeatureGeneration(false); + } catch (RuntimeException e) { + // Exception may happen if world got unloaded unorderly + e.printStackTrace(); + } } } else { if (experimentalWorldGenerator != null) { - experimentalWorldGenerator.stop(); + try { + experimentalWorldGenerator.stop(); + } catch (RuntimeException e) { + e.printStackTrace(); + } experimentalWorldGenerator = null; } } @@ -157,9 +166,7 @@ public class LodWorldGenerator IWorldWrapper serverWorld = LodUtil.getServerWorldFromDimension(lodDim.dimension); PosToGenerateContainer posToGenerate = lodDim.getPosToGenerate( - maxChunkGenRequests, - playerPosX, - playerPosZ); + maxChunkGenRequests, playerPosX, playerPosZ, priority); byte detailLevel; int posX; diff --git a/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHandler.java b/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHandler.java index 53a4f8448..a2ddc873a 100644 --- a/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHandler.java +++ b/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHandler.java @@ -21,11 +21,15 @@ package com.seibel.lod.core.handlers; import java.io.*; import java.nio.file.Files; -import java.nio.file.Paths; import java.nio.file.StandardCopyOption; -import java.util.Arrays; +import java.time.Duration; +import java.time.Instant; +import java.util.ConcurrentModificationException; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.compress.compressors.xz.XZCompressorInputStream; import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream; @@ -39,7 +43,7 @@ import com.seibel.lod.core.objects.lod.RegionPos; import com.seibel.lod.core.objects.lod.VerticalLevelContainer; import com.seibel.lod.core.util.LodThreadFactory; import com.seibel.lod.core.util.LodUtil; -import com.seibel.lod.core.util.ThreadMapUtil; + /** * This object handles creating LodRegions @@ -84,9 +88,10 @@ public class LodDimensionFileHandler * Allow saving asynchronously, but never try to save multiple regions * at a time */ - private final ExecutorService fileWritingThreadPool = Executors.newSingleThreadExecutor(new LodThreadFactory(this.getClass().getSimpleName())); - + private AtomicBoolean isFileWritingThreadRunning = new AtomicBoolean(false); + private ExecutorService fileWritingThreadPool = Executors.newSingleThreadExecutor(new LodThreadFactory(this.getClass().getSimpleName())); + private ConcurrentHashMap regionToSave = new ConcurrentHashMap(); public LodDimensionFileHandler(File newSaveFolder, LodDimension newLodDimension) @@ -112,7 +117,13 @@ public class LodDimensionFileHandler */ public LodRegion loadRegionFromFile(byte detailLevel, RegionPos regionPos, DistanceGenerationMode generationMode, VerticalQuality verticalQuality) { - LodRegion region = new LodRegion((byte) (LodUtil.REGION_DETAIL_LEVEL+1), regionPos, generationMode, verticalQuality); + // Get one from the region hot cache + LodRegion region = regionToSave.get(regionPos); + if (region!=null && region.getMinDetailLevel()<=detailLevel && + region.getGenerationMode().compareTo(generationMode)>=0 && + region.getVerticalQuality().compareTo(verticalQuality)>=0) + return region; // The current hot cache to-be-saved region match our requirement. + region = new LodRegion((byte) (LodUtil.REGION_DETAIL_LEVEL+1), regionPos, generationMode, verticalQuality); return loadRegionFromFile(detailLevel, region, generationMode, verticalQuality); } @@ -123,14 +134,12 @@ public class LodDimensionFileHandler public LodRegion loadRegionFromFile(byte detailLevel, LodRegion region, DistanceGenerationMode generationMode, VerticalQuality verticalQuality) { if (region.getGenerationMode().compareTo(generationMode)<0 || region.getVerticalQuality().compareTo(verticalQuality)<0) { - //TODO: add flush and save region for old one + regionToSave.put(region.getRegionPos(), region); //FIXME: The hashMap key should prob be a {regionPos,VertQual} pair. region = new LodRegion((byte) (LodUtil.REGION_DETAIL_LEVEL+1), region.getRegionPos(), generationMode, verticalQuality); } int regionX = region.regionPosX; int regionZ = region.regionPosZ; - - for (byte tempDetailLevel = (byte) (region.getMinDetailLevel()-1); tempDetailLevel >= detailLevel; tempDetailLevel--) { @@ -214,33 +223,88 @@ public class LodDimensionFileHandler // Save to File // //==============// - /** Save all dirty regions in this LodDimension to file */ - public void saveDirtyRegionsToFileAsync() - { - fileWritingThreadPool.execute(saveDirtyRegionsThread); + public void addRegionsToSave(LodRegion r) { + regionToSave.put(r.getRegionPos(), r); } - private final Thread saveDirtyRegionsThread = new Thread(() -> + /** Save all dirty regions in this LodDimension to file */ + public void saveDirtyRegionsToFile(boolean blockUntilFinished) { - try + for (int i = 0; i < lodDimension.getWidth(); i++) { - for (int i = 0; i < lodDimension.getWidth(); i++) + for (int j = 0; j < lodDimension.getWidth(); j++) { - for (int j = 0; j < lodDimension.getWidth(); j++) + LodRegion r = lodDimension.getRegionByArrayIndex(i, j); + + if (r != null && r.needSaving) { - if (lodDimension.GetIsRegionDirty(i, j) && lodDimension.getRegionByArrayIndex(i, j) != null) - { - saveRegionToFile(lodDimension.getRegionByArrayIndex(i, j)); - lodDimension.SetIsRegionDirty(i, j, false); - } + r.needSaving = false; + regionToSave.put(r.getRegionPos(), r); } } } - catch (Exception e) - { - e.printStackTrace(); + trySaveRegionsToBeSaved(); + if (blockUntilFinished) { + ClientApi.LOGGER.info("Blocking until lod file save finishes!"); + try { + fileWritingThreadPool.shutdown(); + boolean worked = fileWritingThreadPool.awaitTermination(30, TimeUnit.SECONDS); + if (!worked) + ClientApi.LOGGER.error("File writing timed out! File data may not be saved correctly and may cause corruptions!!!"); + } catch (InterruptedException e) { + ClientApi.LOGGER.error("File writing wait is interrupted! File data may not be saved correctly and may cause corruptions!!!"); + e.printStackTrace(); + } finally { + fileWritingThreadPool = Executors.newSingleThreadExecutor(new LodThreadFactory(this.getClass().getSimpleName())); + } } - }); + } + + public void trySaveRegionsToBeSaved() { + if (regionToSave.isEmpty()) return; + // Use Memory order Acquire to acquire any memory changes on getting this boolean + // (Corresponding call is the this::writerMain(...)::...setRelease(false);) + boolean haventStarted = !isFileWritingThreadRunning.compareAndExchangeAcquire(false, true); + if (haventStarted) { + // We acquired the atomic lock. + fileWritingThreadPool.execute(this::writerMain); + } + } + + private void writerMain() { + // Use Memory order Relaxed as no additional memory changes needed to be visible. + // (This is just a safety checks) + boolean isStarted = isFileWritingThreadRunning.getPlain(); + if (!isStarted) throw new ConcurrentModificationException("WriterMain Triggered but the thead state is not started!?"); + ClientApi.LOGGER.info("Lod File Writer started. To-be-written-regions: "+regionToSave.size()); + Instant start = Instant.now(); + // Note: Since regionToSave is a ConcurrentHashMap, and the .values() return one that support concurrency, + // this for loop should be safe and loop until all values are gone. + while (!regionToSave.isEmpty()) { + for (LodRegion r : regionToSave.values()) { + //Check if the data has been swapped out right under me. Otherwise remove it from the entry + if (!regionToSave.remove(r.getRegionPos(), r)) continue; + try + { + Instant i = Instant.now(); + ClientApi.LOGGER.info("Lod: Saving Region "+r.getRegionPos()); + saveRegionToFile(r); + Instant j = Instant.now(); + Duration d = Duration.between(i, j); + ClientApi.LOGGER.info("Lod: Region "+r.getRegionPos()+" save finish. Took "+d); + } + catch (Exception e) + { + e.printStackTrace(); + } + } + } + Instant end = Instant.now(); + ClientApi.LOGGER.info("Lod File Writer completed. Took "+Duration.between(start, end)); + // Use Memory order Release to release any memory changes on setting this boolean + // (Corresponding call is the this::saveRegions(...)::...compareAndExchangeAcquire(false, true);) + isFileWritingThreadRunning.setRelease(false); + } /** * Save a specific region to disk.
@@ -256,7 +320,7 @@ public class LodDimensionFileHandler { // Get the old file File oldFile = getRegionFile(region.regionPosX, region.regionPosZ, region.getGenerationMode(), detailLevel, region.getVerticalQuality()); - ClientApi.LOGGER.debug("saving region [" + region.regionPosX + ", " + region.regionPosZ + "] to file."); + ClientApi.LOGGER.debug("saving region [" + region.regionPosX + ", " + region.regionPosZ + "] detail "+detailLevel+" to file."); boolean isFileFullyGened = false; // make sure the file and folder exists diff --git a/src/main/java/com/seibel/lod/core/objects/PosToGenerateContainer.java b/src/main/java/com/seibel/lod/core/objects/PosToGenerateContainer.java index 730c9d6cb..ad098aafc 100644 --- a/src/main/java/com/seibel/lod/core/objects/PosToGenerateContainer.java +++ b/src/main/java/com/seibel/lod/core/objects/PosToGenerateContainer.java @@ -31,7 +31,7 @@ public class PosToGenerateContainer { private final int playerPosX; private final int playerPosZ; - private final byte farMinDetail; + public final byte farMinDetail; private int nearSize; private int farSize; diff --git a/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java b/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java index 8c5aa7f2c..b249e95fe 100644 --- a/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java +++ b/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java @@ -38,13 +38,13 @@ import com.seibel.lod.core.util.LodThreadFactory; import com.seibel.lod.core.util.LodUtil; import com.seibel.lod.core.util.SingletonHandler; import com.seibel.lod.core.wrapperInterfaces.IWrapperFactory; -import com.seibel.lod.core.wrapperInterfaces.chunk.AbstractChunkPosWrapper; import com.seibel.lod.core.wrapperInterfaces.config.ILodConfigWrapperSingleton; import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftWrapper; import com.seibel.lod.core.wrapperInterfaces.world.IDimensionTypeWrapper; import com.seibel.lod.core.wrapperInterfaces.world.IWorldWrapper; +//FIXME: Race condition on lodDim move/resize! /** * This object holds all loaded LOD regions @@ -62,7 +62,6 @@ public class LodDimension { private static final ILodConfigWrapperSingleton CONFIG = SingletonHandler.get(ILodConfigWrapperSingleton.class); private static final IMinecraftWrapper MC = SingletonHandler.get(IMinecraftWrapper.class); - private static final IWrapperFactory FACTORY = SingletonHandler.get(IWrapperFactory.class); public final IDimensionTypeWrapper dimension; @@ -77,11 +76,8 @@ public class LodDimension public volatile LodRegion[][] regions; /** stores if the region at the given x and z index needs to be saved to disk */ - private volatile boolean[][] isRegionDirty; /** stores if the region at the given x and z index needs to be regenerated */ // Use int because I need Tri state: - // 0: both buffer good. 1: the displaying buffer good. 2: both buffer bad. - private volatile int[][] regenRegionBuffer; /** * if true that means there are regions in this dimension @@ -140,8 +136,6 @@ public class LodDimension regions = new LodRegion[width][width]; - isRegionDirty = new boolean[width][width]; - regenRegionBuffer = new int[width][width]; center = new RegionPos(0, 0); } @@ -158,6 +152,7 @@ public class LodDimension public synchronized void move(RegionPos regionOffset) { ClientApi.LOGGER.info("LodDim MOVE. Offset: "+regionOffset); + saveDirtyRegionsToFile(false); //async add dirty regions to be saved. int xOffset = regionOffset.x; int zOffset = regionOffset.z; @@ -169,7 +164,6 @@ public class LodDimension for (int x = 0; x < width; x++) for (int z = 0; z < width; z++) { regions[x][z] = null; - regenRegionBuffer[x][z] = 0; } // update the new center center.x += xOffset; @@ -187,14 +181,10 @@ public class LodDimension { for (int z = 0; z < width; z++) { - if (x + xOffset < width) { + if (x + xOffset < width) regions[x][z] = regions[x + xOffset][z]; - regenRegionBuffer[x][z] = regenRegionBuffer[x + xOffset][z]; - } - else { + else regions[x][z] = null; - regenRegionBuffer[x][z] = 0; - } } } } @@ -205,14 +195,11 @@ public class LodDimension { for (int z = 0; z < width; z++) { - if (x + xOffset >= 0) { + if (x + xOffset >= 0) regions[x][z] = regions[x + xOffset][z]; - regenRegionBuffer[x][z] = regenRegionBuffer[x + xOffset][z]; - } - else { + else regions[x][z] = null; - regenRegionBuffer[x][z] = 0; - } + } } } @@ -225,14 +212,10 @@ public class LodDimension { for (int z = 0; z < width; z++) { - if (z + zOffset < width) { + if (z + zOffset < width) regions[x][z] = regions[x][z + zOffset]; - regenRegionBuffer[x][z] = regenRegionBuffer[x][z + zOffset]; - } - else { + else regions[x][z] = null; - regenRegionBuffer[x][z] = 0; - } } } } @@ -243,15 +226,10 @@ public class LodDimension { for (int z = width - 1; z >= 0; z--) { - if (z + zOffset >= 0) { + if (z + zOffset >= 0) regions[x][z] = regions[x][z + zOffset]; - regenRegionBuffer[x][z] = regenRegionBuffer[x][z + zOffset]; - } - else { + else regions[x][z] = null; - regenRegionBuffer[x][z] = 0; - } - } } } } @@ -378,18 +356,17 @@ public class LodDimension byte minAllowedDetailLevel; regionX = (x + center.x) - halfWidth; regionZ = (z + center.z) - halfWidth; - - if (regions[x][z] != null) { + LodRegion region = regions[x][z]; + if (region != null) { // check what detail level this region should be // and cut it if it is higher then that minDistance = LevelPosUtil.minDistance(LodUtil.REGION_DETAIL_LEVEL, regionX, regionZ, playerPosX, playerPosZ); detail = DetailDistanceUtil.getTreeCutDetailFromDistance(minDistance); minAllowedDetailLevel = DetailDistanceUtil.getCutLodDetail(detail); - - if (regions[x][z].getMinDetailLevel() < minAllowedDetailLevel) { - regions[x][z].cutTree(minAllowedDetailLevel); - regenRegionBuffer[x][z] = 2; + if (region.getMinDetailLevel() < minAllowedDetailLevel) { + region.cutTree(minAllowedDetailLevel); + region.needRegenBuffer = 2; regenDimensionBuffers = true; } } @@ -436,12 +413,15 @@ public class LodDimension boolean updated = false; if (region == null) { - regions[x][z] = getRegionFromFile(regionPos, minDetail, generationMode, verticalQuality); + region = getRegionFromFile(regionPos, minDetail, generationMode, verticalQuality); + regions[x][z] = region; updated = true; } else if (region.getGenerationMode().compareTo(generationMode) < 0 || region.getVerticalQuality() != verticalQuality || region.getMinDetailLevel() > minDetail) { - regions[x][z] = getRegionFromFile(regions[x][z], minDetail, generationMode, verticalQuality); + // The 'getRegionFromFile' will flush and save the region if it returns a new one + region = getRegionFromFile(regions[x][z], minDetail, generationMode, verticalQuality); + regions[x][z] = region; updated = true; } else if (region.lastMaxDetailLevel != maxDetail) { region.lastMaxDetailLevel = maxDetail; @@ -449,9 +429,8 @@ public class LodDimension } else if (region.lastMaxDetailLevel != region.getMinDetailLevel()) { updated = true; } - if (updated) { - regenRegionBuffer[x][z] = 2; + region.needRegenBuffer = 2; regenDimensionBuffers = true; } }); @@ -462,55 +441,12 @@ public class LodDimension cutAndExpandThread.execute(thread); } - /** - * Use addVerticalData when possible. - * Add the given LOD to this dimension at the coordinate - * stored in the LOD. If an LOD already exists at the given - * coordinate it will be overwritten. - */ - public Boolean addData(byte detailLevel, int posX, int posZ, int verticalIndex, long data, boolean dontSave) - { - int regionPosX = LevelPosUtil.getRegion(detailLevel, posX); - int regionPosZ = LevelPosUtil.getRegion(detailLevel, posZ); - - // don't continue if the region can't be saved - LodRegion region = getRegion(regionPosX, regionPosZ); - if (region == null) - return false; - - boolean nodeAdded = region.addData(detailLevel, posX, posZ, verticalIndex, data); - - // only save valid LODs to disk - if (!dontSave && fileHandler != null) - { - try - { - // mark the region as dirty, so it will be saved to disk - int xIndex = (regionPosX - center.x) + halfWidth; - int zIndex = (regionPosZ - center.z) + halfWidth; - - isRegionDirty[xIndex][zIndex] = true; - regenRegionBuffer[xIndex][zIndex] = 2; - regenDimensionBuffers = true; - } - catch (ArrayIndexOutOfBoundsException e) - { - e.printStackTrace(); - // If this happens, the method was probably - // called when the dimension was changing size. - // Hopefully this shouldn't be an issue. - } - } - - return nodeAdded; - } - /** * Add whole column of LODs to this dimension at the coordinate * stored in the LOD. If an LOD already exists at the given * coordinate it will be overwritten. */ - public Boolean addVerticalData(byte detailLevel, int posX, int posZ, long[] data, boolean dontSave) + public Boolean addVerticalData(byte detailLevel, int posX, int posZ, long[] data) { int regionPosX = LevelPosUtil.getRegion(detailLevel, posX); int regionPosZ = LevelPosUtil.getRegion(detailLevel, posZ); @@ -521,44 +457,28 @@ public class LodDimension return false; boolean nodeAdded = region.addVerticalData(detailLevel, posX, posZ, data); - - // only save valid LODs to disk - if (!dontSave && fileHandler != null) - { - try - { - // mark the region as dirty, so it will be saved to disk - int xIndex = (regionPosX - center.x) + halfWidth; - int zIndex = (regionPosZ - center.z) + halfWidth; - - isRegionDirty[xIndex][zIndex] = true; - regenRegionBuffer[xIndex][zIndex] = 2; - regenDimensionBuffers = true; - } - catch (ArrayIndexOutOfBoundsException e) - { - e.printStackTrace(); - // If this happens, the method was probably - // called when the dimension was changing size. - // Hopefully this shouldn't be an issue. - } + if (nodeAdded) { + region.needRegenBuffer = 2; + region.needSaving = true; + regenDimensionBuffers = true; } - return nodeAdded; } /** marks the region at the given region position to have its buffer rebuilt */ public void markRegionBufferToRegen(int xRegion, int zRegion) { - int xIndex = (xRegion - center.x) + halfWidth; - int zIndex = (zRegion - center.z) + halfWidth; - regenRegionBuffer[xIndex][zIndex] = 2; + LodRegion r = getRegion(xRegion,zRegion); + if (r!=null) { + r.needRegenBuffer = 2; + regenDimensionBuffers = true; + } } /** * Returns every position that need to be generated based on the position of the player */ - public PosToGenerateContainer getPosToGenerate(int maxDataToGenerate, int playerBlockPosX, int playerBlockPosZ) + public PosToGenerateContainer getPosToGenerate(int maxDataToGenerate, int playerBlockPosX, int playerBlockPosZ, GenerationPriority priority) { PosToGenerateContainer posToGenerate; posToGenerate = new PosToGenerateContainer((byte) 8, maxDataToGenerate, playerBlockPosX, playerBlockPosZ); @@ -566,7 +486,7 @@ public class LodDimension //All of this is handled directly by the region, which scan every pos from top to bottom of the quad tree LodRegion lodRegion = regions[x][z]; if (lodRegion != null) - lodRegion.getPosToGenerate(posToGenerate, playerBlockPosX, playerBlockPosZ); + lodRegion.getPosToGenerate(posToGenerate, playerBlockPosX, playerBlockPosZ, priority); }); return posToGenerate; } @@ -656,8 +576,9 @@ public class LodDimension LodRegion region = getRegion(xRegion, zRegion); if (region == null) return; - markRegionBufferToRegen(xRegion, zRegion); region.clear(detailLevel, posX, posZ); + region.needRegenBuffer = 2; + regenDimensionBuffers = true; } /** @@ -666,16 +587,12 @@ public class LodDimension */ public boolean getAndClearRegionNeedBufferRegen(int regionX, int regionZ) { - //FIXME: Use actual atomics on regenRegionBuffer - //FIXME: Race condition on lodDim move/resize! - int xIndex = (regionX - center.x) + halfWidth; - int zIndex = (regionZ - center.z) + halfWidth; - - if (xIndex < 0 || xIndex >= width || zIndex < 0 || zIndex >= width) - return false; - int i = regenRegionBuffer[xIndex][zIndex]; + //FIXME: Use actual atomics on needRegenBuffer + LodRegion region = getRegion(regionX, regionZ); + if (region == null) return false; + int i = region.needRegenBuffer; if (i > 0) { - regenRegionBuffer[xIndex][zIndex]--; + region.needRegenBuffer--; return true; } return false; @@ -696,11 +613,10 @@ public class LodDimension int xRegion = LevelPosUtil.getRegion(detailLevel, posX); int zRegion = LevelPosUtil.getRegion(detailLevel, posZ); LodRegion region = getRegion(xRegion, zRegion); - if (region == null) - return; - markRegionBufferToRegen(xRegion, zRegion); - + if (region == null) return; region.updateArea(detailLevel, posX, posZ); + region.needRegenBuffer = 2; + regenDimensionBuffers = true; } /** Returns true if a region exists at the given LevelPos */ @@ -732,9 +648,10 @@ public class LodDimension } /** Save all dirty regions in this LodDimension to file. */ - public void saveDirtyRegionsToFileAsync() + public void saveDirtyRegionsToFile(boolean blockUntilFinished) { - fileHandler.saveDirtyRegionsToFileAsync(); + if (fileHandler == null) return; + fileHandler.saveDirtyRegionsToFile(blockUntilFinished); } @@ -789,13 +706,6 @@ public class LodDimension halfWidth = width/ 2; regions = new LodRegion[width][width]; - isRegionDirty = new boolean[width][width]; - regenRegionBuffer = new int[width][width]; - - // populate isRegionDirty - for (int i = 0; i < width; i++) - for (int j = 0; j < width; j++) - isRegionDirty[i][j] = false; } @@ -818,14 +728,4 @@ public class LodDimension } return stringBuilder.toString(); } - - public boolean GetIsRegionDirty(int i, int j) - { - return isRegionDirty[i][j]; - } - - public void SetIsRegionDirty(int i, int j, boolean val) - { - isRegionDirty[i][j] = val; - } } diff --git a/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java b/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java index 304444c1d..b29d4a127 100644 --- a/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java +++ b/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java @@ -20,6 +20,7 @@ package com.seibel.lod.core.objects.lod; import com.seibel.lod.core.enums.config.DistanceGenerationMode; +import com.seibel.lod.core.enums.config.GenerationPriority; import com.seibel.lod.core.enums.config.VerticalQuality; import com.seibel.lod.core.objects.PosToGenerateContainer; import com.seibel.lod.core.objects.PosToRenderContainer; @@ -67,6 +68,9 @@ public class LodRegion /** this region's z RegionPos */ public final int regionPosZ; + public volatile int needRegenBuffer = 2; + public volatile boolean needSaving = false; + public LodRegion(byte minDetailLevel, RegionPos regionPos, DistanceGenerationMode generationMode, VerticalQuality verticalQuality) { this.minDetailLevel = minDetailLevel; @@ -81,73 +85,8 @@ public class LodRegion { dataContainer[lod] = new VerticalLevelContainer(lod); } - - boolean fileFound = false; - - /* - preGeneratedChunkPos = new boolean[32 * 32]; - if (MinecraftWrapper.INSTANCE.hasSinglePlayerServer() && LodConfig.CLIENT.worldGenerator.useExperimentalPreGenLoading.get()) - { - File regionFileDirHead; - File regionFileDirParent; - // local world - - ServerWorld serverWorld = LodUtil.getServerWorldFromDimension(MinecraftWrapper.INSTANCE.getCurrentDimension()); - - // provider needs a separate variable to prevent - // the compiler from complaining - StringBuilder string = new StringBuilder(); - try - { - ServerChunkProvider provider = serverWorld.getChunkSource(); - - //System.out.println(provider.dataStorage.dataFolder); - regionFileDirHead = new File(provider.dataStorage.dataFolder.getCanonicalFile().getParentFile().toPath().toAbsolutePath().toString() + File.separatorChar + "region", "r." + regionPosZ + "." + regionPosX + ".mca"); - if (regionFileDirHead.exists()) - { - regionFileDirParent = regionFileDirHead.getParentFile(); - //string.append(regionFileDirParent.toString()); - string.append(regionFileDirHead); - RegionFile regionFile = new RegionFile(regionFileDirHead, regionFileDirParent, true); - for (int x = 0; x < 32; x++) - { - for (int z = 0; z < 32; z++) - { - preGeneratedChunkPos[x * 32 + z] = regionFile.doesChunkExist(new ChunkPos(regionPosX * 32 + x, regionPosZ * 32 + z)); - } - } - - string.append("region " + regionPosX + " " + regionPosZ + "\n"); - for (int x = 0; x < 32; x++) - { - for (int z = 0; z < 32; z++) - { - //regionFile.doesChunkExist() - string.append(preGeneratedChunkPos[x * 32 + z] + "\t"); - } - string.append("\n"); - } - regionFile.close(); - } - } - catch (Exception e) - { - e.printStackTrace(); - } - System.out.println(string); - }*/ - } - - /** Return true if the chunk has been pregenerated in game */ - //public boolean isChunkPreGenerated(int xChunkPos, int zChunkPos) - //{ - // xChunkPos = LevelPosUtil.getRegionModule(LodUtil.CHUNK_DETAIL_LEVEL, xChunkPos); - // zChunkPos = LevelPosUtil.getRegionModule(LodUtil.CHUNK_DETAIL_LEVEL, zChunkPos); - // return preGeneratedChunkPos[xChunkPos * 32 + zChunkPos]; - //} - /** * Inserts the data point into the region. *

@@ -229,9 +168,9 @@ public class LodRegion * TODO why don't we return the posToGenerate, it would make this easier to understand */ public void getPosToGenerate(PosToGenerateContainer posToGenerate, - int playerBlockPosX, int playerBlockPosZ) + int playerBlockPosX, int playerBlockPosZ, GenerationPriority priority) { - getPosToGenerate(posToGenerate, LodUtil.REGION_DETAIL_LEVEL, 0, 0, playerBlockPosX, playerBlockPosZ); + getPosToGenerate(posToGenerate, LodUtil.REGION_DETAIL_LEVEL, 0, 0, playerBlockPosX, playerBlockPosZ, priority); } @@ -242,7 +181,7 @@ public class LodRegion * TODO why don't we return the posToGenerate, it would make this easier to understand */ private void getPosToGenerate(PosToGenerateContainer posToGenerate, byte detailLevel, - int childOffsetPosX, int childOffsetPosZ, int playerPosX, int playerPosZ) + int childOffsetPosX, int childOffsetPosZ, int playerPosX, int playerPosZ, GenerationPriority priority) { // equivalent to 2^(...) int size = 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel); @@ -275,11 +214,14 @@ public class LodRegion { for (int x = 0; x <= 1; x++) for (int z = 0; z <= 1; z++) - getPosToGenerate(posToGenerate, childDetailLevel, childPosX + x, childPosZ + z, playerPosX, playerPosZ); - } - else - { - getPosToGenerate(posToGenerate, childDetailLevel, childPosX, childPosZ, playerPosX, playerPosZ); + getPosToGenerate(posToGenerate, childDetailLevel, childPosX + x, childPosZ + z, playerPosX, playerPosZ, priority); + } else if (priority == GenerationPriority.FAR_FIRST && detailLevel == posToGenerate.farMinDetail) { + if (!doesDataExist(detailLevel, childOffsetPosX, childOffsetPosZ)) + posToGenerate.addPosToGenerate(detailLevel, childOffsetPosX + regionPosX * size, childOffsetPosZ + regionPosZ * size); + else + getPosToGenerate(posToGenerate, childDetailLevel, childPosX, childPosZ, playerPosX, playerPosZ, priority); + } else { + getPosToGenerate(posToGenerate, childDetailLevel, childPosX, childPosZ, playerPosX, playerPosZ, priority); } } } diff --git a/src/main/java/com/seibel/lod/core/objects/lod/LodWorld.java b/src/main/java/com/seibel/lod/core/objects/lod/LodWorld.java index 9897d297d..69c21f23f 100644 --- a/src/main/java/com/seibel/lod/core/objects/lod/LodWorld.java +++ b/src/main/java/com/seibel/lod/core/objects/lod/LodWorld.java @@ -92,6 +92,7 @@ public class LodWorld public void deselectWorld() { worldName = NO_WORLD_LOADED; + saveAllDimensions(true); // Make sure all dims are saved. This will block threads lodDimensions = null; isWorldLoaded = false; } @@ -106,7 +107,8 @@ public class LodWorld if (lodDimensions == null) return; - lodDimensions.put(newDimension.dimension, newDimension); + LodDimension oldDim = lodDimensions.put(newDimension.dimension, newDimension); + if (oldDim != null) oldDim.saveDirtyRegionsToFile(true); } /** @@ -129,7 +131,7 @@ public class LodWorld if (lodDimensions == null) return; - saveAllDimensions(); + saveAllDimensions(true); //block until saving is done for (IDimensionTypeWrapper key : lodDimensions.keySet()) lodDimensions.get(key).setRegionWidth(newRegionWidth); @@ -138,7 +140,7 @@ public class LodWorld /** * Requests all dimensions save any dirty regions they may have. */ - public void saveAllDimensions() + public void saveAllDimensions(boolean isBlocking) { if (lodDimensions == null) return; @@ -147,8 +149,10 @@ public class LodWorld // but that requires a LodDimension.hasDirtyRegions() method or something similar ClientApi.LOGGER.info("Saving LODs"); - for (IDimensionTypeWrapper key : lodDimensions.keySet()) - lodDimensions.get(key).saveDirtyRegionsToFileAsync(); + for (IDimensionTypeWrapper key : lodDimensions.keySet()) { + lodDimensions.get(key).saveDirtyRegionsToFile(isBlocking); + } + //FIXME: This should block until file is saved. } diff --git a/src/main/java/com/seibel/lod/core/objects/lod/RegionPos.java b/src/main/java/com/seibel/lod/core/objects/lod/RegionPos.java index f50754d02..6260cb418 100644 --- a/src/main/java/com/seibel/lod/core/objects/lod/RegionPos.java +++ b/src/main/java/com/seibel/lod/core/objects/lod/RegionPos.java @@ -82,10 +82,29 @@ public class RegionPos .offset(LodUtil.CHUNK_WIDTH / 2, 0, LodUtil.CHUNK_WIDTH / 2); } + @Override + public boolean equals(Object o) { + // If the object is compared with itself then return true + if (o == this) { + return true; + } + // Check if o is an instance of RegionPos or not + if (!(o instanceof RegionPos)) { + return false; + } + RegionPos c = (RegionPos) o; + return c.x==x &&c.z==z; + } + @Override public String toString() { return "(" + x + "," + z + ")"; } + + @Override + public int hashCode() { + return Long.hashCode((long)(x) << Integer.BYTES + z); + } } diff --git a/src/main/java/com/seibel/lod/core/render/LodRenderer.java b/src/main/java/com/seibel/lod/core/render/LodRenderer.java index 47d6b0f22..c78d41c06 100644 --- a/src/main/java/com/seibel/lod/core/render/LodRenderer.java +++ b/src/main/java/com/seibel/lod/core/render/LodRenderer.java @@ -382,7 +382,7 @@ public class LodRenderer } /** Create all buffers that will be used. */ - public void setupBuffers(LodDimension lodDim) + public void setupBuffers() { lodBufferBuilderFactory.allBuffersRequireReset = true; } From c664564fb061b633d97daf23851c9847cb00c556 Mon Sep 17 00:00:00 2001 From: tom lee Date: Thu, 6 Jan 2022 16:27:28 +0800 Subject: [PATCH 09/20] Fixed FAR_FIRST gen. Impl proper cleanups on many place. --- .../com/seibel/lod/core/api/ApiShared.java | 3 + .../com/seibel/lod/core/api/EventApi.java | 21 +- .../worldGeneration/LodWorldGenerator.java | 19 +- .../lod/core/objects/lod/LodRegion.java | 461 ++++++++---------- .../core/objects/opengl/LodVertexBuffer.java | 7 +- .../com/seibel/lod/core/render/GLProxy.java | 20 +- 6 files changed, 270 insertions(+), 261 deletions(-) diff --git a/src/main/java/com/seibel/lod/core/api/ApiShared.java b/src/main/java/com/seibel/lod/core/api/ApiShared.java index 36f8635b5..6dd1cb740 100644 --- a/src/main/java/com/seibel/lod/core/api/ApiShared.java +++ b/src/main/java/com/seibel/lod/core/api/ApiShared.java @@ -43,6 +43,9 @@ public class ApiShared /** Used to determine if the LODs should be regenerated */ public static int previousLodRenderDistance = 0; + /** Signal whether a world is shutting down */ + public static volatile boolean isShuttingDown = false; + private ApiShared() diff --git a/src/main/java/com/seibel/lod/core/api/EventApi.java b/src/main/java/com/seibel/lod/core/api/EventApi.java index 429252e82..ca4f48623 100644 --- a/src/main/java/com/seibel/lod/core/api/EventApi.java +++ b/src/main/java/com/seibel/lod/core/api/EventApi.java @@ -23,9 +23,11 @@ import org.lwjgl.glfw.GLFW; import com.seibel.lod.core.builders.lodBuilding.LodBuilder; import com.seibel.lod.core.builders.worldGeneration.LodWorldGenerator; +import com.seibel.lod.core.enums.WorldType; import com.seibel.lod.core.enums.config.DistanceGenerationMode; import com.seibel.lod.core.objects.lod.LodDimension; import com.seibel.lod.core.objects.lod.RegionPos; +import com.seibel.lod.core.render.GLProxy; import com.seibel.lod.core.render.LodRenderer; import com.seibel.lod.core.util.DataPointUtil; import com.seibel.lod.core.util.DetailDistanceUtil; @@ -80,6 +82,7 @@ public class EventApi LodDimension lodDim = ApiShared.lodWorld.getLodDimension(MC.getCurrentDimension()); if (lodDim == null) return; + if (ApiShared.isShuttingDown) return; LodWorldGenerator.INSTANCE.queueGenerationRequests(lodDim, ApiShared.lodBuilder); } @@ -101,9 +104,16 @@ public class EventApi ApiShared.lodWorld.saveAllDimensions(false); // Do an async save. } + private boolean isCurrentlyOnSinglePlayerServer = false; + /** This is also called when a new dimension loads */ public void worldLoadEvent(IWorldWrapper world) { + ClientApi.LOGGER.info("WorldLoadEvent called here for "+ (world.getWorldType() == WorldType.ClientWorld ? "clientLevel" : "serverLevel"), new RuntimeException()); + // Always ignore ServerWorld event + if (world.getWorldType() == WorldType.ServerWorld) return; + isCurrentlyOnSinglePlayerServer = MC.hasSinglePlayerServer(); + ApiShared.isShuttingDown = false; DataPointUtil.WORLD_HEIGHT = world.getHeight(); LodBuilder.MIN_WORLD_HEIGHT = world.getMinHeight(); // This updates the World height @@ -119,8 +129,12 @@ public class EventApi } /** This is also called when the user disconnects from a server+ */ - public void worldUnloadEvent() + public void worldUnloadEvent(IWorldWrapper world) { + ClientApi.LOGGER.info("WorldUnloadEvent called here for "+ (world.getWorldType() == WorldType.ClientWorld ? "clientLevel" : "serverLevel"), new RuntimeException()); + // If it's single player, ignore the client side world unload event + // Note: using this as often API call unload event AFTER setting MC to not be in a singlePlayerServer + if (isCurrentlyOnSinglePlayerServer && world.getWorldType() == WorldType.ClientWorld) return; // the player just unloaded a world/dimension ThreadMapUtil.clearMaps(); // ClientApi.renderer.markForCleanup(); @@ -152,15 +166,17 @@ public class EventApi // if this isn't done unfinished tasks may be left in the queue // preventing new LodChunks form being generated + ApiShared.isShuttingDown = true; LodWorldGenerator.INSTANCE.restartExecutorService(); LodWorldGenerator.INSTANCE.numberOfChunksWaitingToGenerate.set(0); - ApiShared.lodWorld.deselectWorld(); + ApiShared.lodWorld.deselectWorld(); // This force a save // prevent issues related to the buffer builder // breaking or retaining previous data when changing worlds. ClientApi.renderer.destroyBuffers(); ClientApi.renderer.requestCleanup(); + GLProxy.ensureAllGLJobCompleted(); recalculateWidths = true; // TODO: Check if after the refactoring, is this still needed ClientApi.renderer = new LodRenderer(ApiShared.lodBufferBuilderFactory); @@ -204,6 +220,7 @@ public class EventApi } } + // NOTE: This is being called from Render Thread. /** Re-centers the given LodDimension if it needs to be. */ public void playerMoveEvent(LodDimension lodDim) { diff --git a/src/main/java/com/seibel/lod/core/builders/worldGeneration/LodWorldGenerator.java b/src/main/java/com/seibel/lod/core/builders/worldGeneration/LodWorldGenerator.java index 28321ee87..273d6f118 100644 --- a/src/main/java/com/seibel/lod/core/builders/worldGeneration/LodWorldGenerator.java +++ b/src/main/java/com/seibel/lod/core/builders/worldGeneration/LodWorldGenerator.java @@ -23,9 +23,11 @@ import java.util.HashSet; import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.seibel.lod.core.api.ClientApi; import com.seibel.lod.core.builders.lodBuilding.LodBuilder; import com.seibel.lod.core.enums.config.DistanceGenerationMode; import com.seibel.lod.core.enums.config.GenerationPriority; @@ -378,10 +380,21 @@ public class LodWorldGenerator if (genSubThreads != null && !genSubThreads.isShutdown()) { - genSubThreads.shutdownNow(); + ClientApi.LOGGER.info("Blocking until generator sub threads terminated!!"); + try { + mainGenThread.shutdownNow(); + genSubThreads.shutdownNow(); + boolean worked = genSubThreads.awaitTermination(30, TimeUnit.SECONDS); + if (!worked) + ClientApi.LOGGER.error("Generator sub threads timed out! May cause crash on game exit due to cleanup failure."); + } catch (InterruptedException e) { + ClientApi.LOGGER.error("Generator sub threads shutdown is interrupted! May cause crash on game exit due to cleanup failure."); + e.printStackTrace(); + } finally { + genSubThreads = Executors.newFixedThreadPool(CONFIG.client().advanced().threading().getNumberOfWorldGenerationThreads(), + new ThreadFactoryBuilder().setNameFormat("Gen-Worker-Thread-%d").build()); + } } - genSubThreads = Executors.newFixedThreadPool(CONFIG.client().advanced().threading().getNumberOfWorldGenerationThreads(), - new ThreadFactoryBuilder().setNameFormat("Gen-Worker-Thread-%d").build()); } } diff --git a/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java b/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java index b29d4a127..936d16d21 100644 --- a/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java +++ b/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java @@ -30,8 +30,9 @@ import com.seibel.lod.core.util.LevelPosUtil; import com.seibel.lod.core.util.LodUtil; /** - * This object holds all loaded LevelContainers acting as a quad tree - * for a given region.

+ * This object holds all loaded LevelContainers acting as a quad tree for a + * given region.
+ *
* * Coordinate Standard:
* Coordinate called posX or posZ are relative LevelPos coordinates
@@ -40,359 +41,325 @@ import com.seibel.lod.core.util.LodUtil; * @author Leonardo Amato * @version 10-10-2021 */ -public class LodRegion -{ +public class LodRegion { /** Number of detail level supported by a region */ private static final byte POSSIBLE_LOD = 10; - - + /** Holds the lowest (least detailed) detail level in this region */ private byte minDetailLevel; public byte lastMaxDetailLevel = LodUtil.REGION_DETAIL_LEVEL; - + /** * This holds all data for this region */ private final LevelContainer[] dataContainer; - + /** This chunk Pos has been generated */ - //private final boolean[] preGeneratedChunkPos; - + // private final boolean[] preGeneratedChunkPos; + /** the generation mode for this region */ private final DistanceGenerationMode generationMode; /** the vertical quality of this region */ private final VerticalQuality verticalQuality; - + /** this region's x RegionPos */ public final int regionPosX; /** this region's z RegionPos */ public final int regionPosZ; - + public volatile int needRegenBuffer = 2; public volatile boolean needSaving = false; - - public LodRegion(byte minDetailLevel, RegionPos regionPos, DistanceGenerationMode generationMode, VerticalQuality verticalQuality) - { + + public LodRegion(byte minDetailLevel, RegionPos regionPos, DistanceGenerationMode generationMode, + VerticalQuality verticalQuality) { this.minDetailLevel = minDetailLevel; this.regionPosX = regionPos.x; this.regionPosZ = regionPos.z; this.verticalQuality = verticalQuality; this.generationMode = generationMode; dataContainer = new LevelContainer[POSSIBLE_LOD]; - + // Initialize all the different matrices - for (byte lod = minDetailLevel; lod <= LodUtil.REGION_DETAIL_LEVEL; lod++) - { + for (byte lod = minDetailLevel; lod <= LodUtil.REGION_DETAIL_LEVEL; lod++) { dataContainer[lod] = new VerticalLevelContainer(lod); } } - + /** * Inserts the data point into the region. *

* TODO this will always return true unless it has + * * @return true if the data was added successfully */ - public boolean addData(byte detailLevel, int posX, int posZ, int verticalIndex, long data) - { + public boolean addData(byte detailLevel, int posX, int posZ, int verticalIndex, long data) { posX = LevelPosUtil.getRegionModule(detailLevel, posX); posZ = LevelPosUtil.getRegionModule(detailLevel, posZ); - + // The dataContainer could have null entries if the // detailLevel changes. if (this.dataContainer[detailLevel] == null) - return false;//this.dataContainer[detailLevel] = new VerticalLevelContainer(detailLevel); - + return false;// this.dataContainer[detailLevel] = new VerticalLevelContainer(detailLevel); + this.dataContainer[detailLevel].addData(data, posX, posZ, verticalIndex); - + return true; } - + /** * Inserts the vertical data into the region. *

* TODO this will always return true unless it has + * * @return true if the data was added successfully */ - public boolean addVerticalData(byte detailLevel, int posX, int posZ, long[] data) - { + public boolean addVerticalData(byte detailLevel, int posX, int posZ, long[] data) { posX = LevelPosUtil.getRegionModule(detailLevel, posX); posZ = LevelPosUtil.getRegionModule(detailLevel, posZ); - + // The dataContainer could have null entries if the // detailLevel changes. if (this.dataContainer[detailLevel] == null) - return false;//this.dataContainer[detailLevel] = new VerticalLevelContainer(detailLevel); - + return false;// this.dataContainer[detailLevel] = new VerticalLevelContainer(detailLevel); + return this.dataContainer[detailLevel].addVerticalData(data, posX, posZ); } - + /** * Get the dataPoint at the given relative position. - * @return the data at the relative pos and detail level, - * 0 if the data doesn't exist. + * + * @return the data at the relative pos and detail level, 0 if the data doesn't + * exist. */ - public long getData(byte detailLevel, int posX, int posZ, int verticalIndex) - { + public long getData(byte detailLevel, int posX, int posZ, int verticalIndex) { posX = LevelPosUtil.getRegionModule(detailLevel, posX); posZ = LevelPosUtil.getRegionModule(detailLevel, posZ); return dataContainer[detailLevel].getData(posX, posZ, verticalIndex); } - + /** * Get the dataPoint at the given relative position. - * @return the data at the relative pos and detail level, - * 0 if the data doesn't exist. + * + * @return the data at the relative pos and detail level, 0 if the data doesn't + * exist. */ - public long getSingleData(byte detailLevel, int posX, int posZ) - { + public long getSingleData(byte detailLevel, int posX, int posZ) { posX = LevelPosUtil.getRegionModule(detailLevel, posX); posZ = LevelPosUtil.getRegionModule(detailLevel, posZ); return dataContainer[detailLevel].getSingleData(posX, posZ); } - + /** * Clears the datapoint at the given relative position */ - public void clear(byte detailLevel, int posX, int posZ) - { + public void clear(byte detailLevel, int posX, int posZ) { posX = LevelPosUtil.getRegionModule(detailLevel, posX); posZ = LevelPosUtil.getRegionModule(detailLevel, posZ); dataContainer[detailLevel].clear(posX, posZ); } - + /** - * This method will fill the posToGenerate array with all levelPos that - * are render-able. + * This method will fill the posToGenerate array with all levelPos that are + * render-able. *

- * TODO why don't we return the posToGenerate, it would make this easier to understand + * TODO why don't we return the posToGenerate, it would make this easier to + * understand */ - public void getPosToGenerate(PosToGenerateContainer posToGenerate, - int playerBlockPosX, int playerBlockPosZ, GenerationPriority priority) - { + public void getPosToGenerate(PosToGenerateContainer posToGenerate, int playerBlockPosX, int playerBlockPosZ, + GenerationPriority priority) { getPosToGenerate(posToGenerate, LodUtil.REGION_DETAIL_LEVEL, 0, 0, playerBlockPosX, playerBlockPosZ, priority); - + } - + /** * A recursive method that fills the posToGenerate array with all levelPos that * need to be generated. *

- * TODO why don't we return the posToGenerate, it would make this easier to understand + * TODO why don't we return the posToGenerate, it would make this easier to + * understand */ - private void getPosToGenerate(PosToGenerateContainer posToGenerate, byte detailLevel, - int childOffsetPosX, int childOffsetPosZ, int playerPosX, int playerPosZ, GenerationPriority priority) - { + private void getPosToGenerate(PosToGenerateContainer posToGenerate, byte detailLevel, int childOffsetPosX, + int childOffsetPosZ, int playerPosX, int playerPosZ, GenerationPriority priority) { // equivalent to 2^(...) int size = 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel); - + // calculate what LevelPos are in range to generate - int minDistance = LevelPosUtil.minDistance(LodUtil.REGION_DETAIL_LEVEL, regionPosX, regionPosZ, playerPosX, playerPosZ); - + int minDistance = LevelPosUtil.minDistance(LodUtil.REGION_DETAIL_LEVEL, regionPosX, regionPosZ, playerPosX, + playerPosZ); + // determine this child's levelPos byte childDetailLevel = (byte) (detailLevel - 1); int childPosX = childOffsetPosX * 2; int childPosZ = childOffsetPosZ * 2; - + int childSize = 1 << (LodUtil.REGION_DETAIL_LEVEL - childDetailLevel); - + byte targetDetailLevel = DetailDistanceUtil.getGenerationDetailFromDistance(minDistance); - if (targetDetailLevel <= detailLevel) - { - if (targetDetailLevel == detailLevel) - { + if (targetDetailLevel <= detailLevel) { + if (targetDetailLevel == detailLevel) { if (!doesDataExist(detailLevel, childOffsetPosX, childOffsetPosZ)) - posToGenerate.addPosToGenerate(detailLevel, childOffsetPosX + regionPosX * size, childOffsetPosZ + regionPosZ * size); - } - else - { - // we want at max one request per chunk (since the world generator creates chunks). - // So for lod smaller than a chunk, only recurse down - // the top right child - - if (detailLevel > LodUtil.CHUNK_DETAIL_LEVEL) - { + posToGenerate.addPosToGenerate(detailLevel, childOffsetPosX + regionPosX * size, + childOffsetPosZ + regionPosZ * size); + } else { + if (priority == GenerationPriority.FAR_FIRST && detailLevel >= posToGenerate.farMinDetail + && !doesDataExist(detailLevel, childOffsetPosX, childOffsetPosZ)) { + posToGenerate.addPosToGenerate(detailLevel, childOffsetPosX + regionPosX * size, + childOffsetPosZ + regionPosZ * size); + } else if (detailLevel > LodUtil.CHUNK_DETAIL_LEVEL) { for (int x = 0; x <= 1; x++) for (int z = 0; z <= 1; z++) - getPosToGenerate(posToGenerate, childDetailLevel, childPosX + x, childPosZ + z, playerPosX, playerPosZ, priority); - } else if (priority == GenerationPriority.FAR_FIRST && detailLevel == posToGenerate.farMinDetail) { - if (!doesDataExist(detailLevel, childOffsetPosX, childOffsetPosZ)) - posToGenerate.addPosToGenerate(detailLevel, childOffsetPosX + regionPosX * size, childOffsetPosZ + regionPosZ * size); - else - getPosToGenerate(posToGenerate, childDetailLevel, childPosX, childPosZ, playerPosX, playerPosZ, priority); + getPosToGenerate(posToGenerate, childDetailLevel, childPosX + x, childPosZ + z, playerPosX, + playerPosZ, priority); } else { - getPosToGenerate(posToGenerate, childDetailLevel, childPosX, childPosZ, playerPosX, playerPosZ, priority); + // we want at max one request per chunk (since the world generator creates + // chunks). + // So for lod smaller than a chunk, only recurse down + // the top right child + getPosToGenerate(posToGenerate, childDetailLevel, childPosX, childPosZ, playerPosX, playerPosZ, + priority); } } } // we have gone beyond the target Detail level // we can stop generating - + } - - + /** - * This method will fill the posToRender array with all levelPos that - * are render-able. + * This method will fill the posToRender array with all levelPos that are + * render-able. *

- * TODO why don't we return the posToRender, it would make this easier to understand + * TODO why don't we return the posToRender, it would make this easier to + * understand */ - public void getPosToRender(PosToRenderContainer posToRender, - int playerPosX, int playerPosZ, boolean requireCorrectDetailLevel) - { - getPosToRender(posToRender, LodUtil.REGION_DETAIL_LEVEL, 0, 0, playerPosX, playerPosZ, requireCorrectDetailLevel); + public void getPosToRender(PosToRenderContainer posToRender, int playerPosX, int playerPosZ, + boolean requireCorrectDetailLevel) { + getPosToRender(posToRender, LodUtil.REGION_DETAIL_LEVEL, 0, 0, playerPosX, playerPosZ, + requireCorrectDetailLevel); } - + /** - * This method will fill the posToRender array with all levelPos that - * are render-able. + * This method will fill the posToRender array with all levelPos that are + * render-able. *

- * TODO why don't we return the posToRender, it would make this easier to understand - * TODO this needs some more comments, James was only able to figure out part of it + * TODO why don't we return the posToRender, it would make this easier to + * understand TODO this needs some more comments, James was only able to figure + * out part of it */ - private void getPosToRender(PosToRenderContainer posToRender, - byte detailLevel, int posX, int posZ, - int playerPosX, int playerPosZ, boolean requireCorrectDetailLevel) - { + private void getPosToRender(PosToRenderContainer posToRender, byte detailLevel, int posX, int posZ, int playerPosX, + int playerPosZ, boolean requireCorrectDetailLevel) { // equivalent to 2^(...) int size = 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel); - + byte desiredLevel; int maxDistance; int minDistance; int childLevel; - - + // calculate the LevelPos that are in range maxDistance = LevelPosUtil.maxDistance(detailLevel, posX, posZ, playerPosX, playerPosZ, regionPosX, regionPosZ); desiredLevel = DetailDistanceUtil.getLodDrawDetail(DetailDistanceUtil.getDrawDetailFromDistance(maxDistance)); minDistance = LevelPosUtil.minDistance(detailLevel, posX, posZ, playerPosX, playerPosZ, regionPosX, regionPosZ); childLevel = DetailDistanceUtil.getLodDrawDetail(DetailDistanceUtil.getDrawDetailFromDistance(minDistance)); - - if (detailLevel == childLevel - 1) + + if (detailLevel == childLevel - 1) { + posToRender.addPosToRender(detailLevel, posX + regionPosX * size, posZ + regionPosZ * size); + } else + // if (desiredLevel > detailLevel) + // { + // we have gone beyond the target Detail level + // we can stop generating + // } else + if (desiredLevel == detailLevel) { + posToRender.addPosToRender(detailLevel, posX + regionPosX * size, posZ + regionPosZ * size); + } else // case where (detailLevel > desiredLevel) { - posToRender.addPosToRender(detailLevel, - posX + regionPosX * size, - posZ + regionPosZ * size); + int childPosX = posX * 2; + int childPosZ = posZ * 2; + byte childDetailLevel = (byte) (detailLevel - 1); + int childrenCount = 0; + + for (int x = 0; x <= 1; x++) { + for (int z = 0; z <= 1; z++) { + if (doesDataExist(childDetailLevel, childPosX + x, childPosZ + z)) { + if (!requireCorrectDetailLevel) + childrenCount++; + else + getPosToRender(posToRender, childDetailLevel, childPosX + x, childPosZ + z, playerPosX, + playerPosZ, requireCorrectDetailLevel); + } + } + } + + if (!requireCorrectDetailLevel) { + // If all the four children exist go deeper + if (childrenCount == 4) { + for (int x = 0; x <= 1; x++) + for (int z = 0; z <= 1; z++) + getPosToRender(posToRender, childDetailLevel, childPosX + x, childPosZ + z, playerPosX, + playerPosZ, requireCorrectDetailLevel); + } else { + posToRender.addPosToRender(detailLevel, posX + regionPosX * size, posZ + regionPosZ * size); + } + } } - else - //if (desiredLevel > detailLevel) - //{ - // we have gone beyond the target Detail level - // we can stop generating - //} else - if (desiredLevel == detailLevel) - { - posToRender.addPosToRender(detailLevel, - posX + regionPosX * size, - posZ + regionPosZ * size); - } - else //case where (detailLevel > desiredLevel) - { - int childPosX = posX * 2; - int childPosZ = posZ * 2; - byte childDetailLevel = (byte) (detailLevel - 1); - int childrenCount = 0; - - for (int x = 0; x <= 1; x++) - { - for (int z = 0; z <= 1; z++) - { - if (doesDataExist(childDetailLevel, childPosX + x, childPosZ + z)) - { - if (!requireCorrectDetailLevel) - childrenCount++; - else - getPosToRender(posToRender, childDetailLevel, childPosX + x, childPosZ + z, playerPosX, playerPosZ, requireCorrectDetailLevel); - } - } - } - - - if (!requireCorrectDetailLevel) - { - // If all the four children exist go deeper - if (childrenCount == 4) - { - for (int x = 0; x <= 1; x++) - for (int z = 0; z <= 1; z++) - getPosToRender(posToRender, childDetailLevel, childPosX + x, childPosZ + z, playerPosX, playerPosZ, requireCorrectDetailLevel); - } - else - { - posToRender.addPosToRender(detailLevel, - posX + regionPosX * size, - posZ + regionPosZ * size); - } - } - } } - - + /** * Updates all children. *

* TODO could this be renamed mergeArea? */ - public void updateArea(byte detailLevel, int posX, int posZ) - { + public void updateArea(byte detailLevel, int posX, int posZ) { int width; int startX; int startZ; - + // Update the level lower or equal to the detail level - for (byte down = (byte) (minDetailLevel+1); down <= detailLevel; down++) - { + for (byte down = (byte) (minDetailLevel + 1); down <= detailLevel; down++) { startX = LevelPosUtil.convert(detailLevel, posX, down); startZ = LevelPosUtil.convert(detailLevel, posZ, down); width = 1 << (detailLevel - down); - - for (int x = 0; x < width; x++) + + for (int x = 0; x < width; x++) for (int z = 0; z < width; z++) update(down, startX + x, startZ + z); } // Update the level higher than the detail level - for (byte up = (byte)(Math.max(detailLevel, minDetailLevel)+1); up <= LodUtil.REGION_DETAIL_LEVEL; up++) - { - update(up, - LevelPosUtil.convert(detailLevel, posX, up), - LevelPosUtil.convert(detailLevel, posZ, up)); + for (byte up = (byte) (Math.max(detailLevel, minDetailLevel) + 1); up <= LodUtil.REGION_DETAIL_LEVEL; up++) { + update(up, LevelPosUtil.convert(detailLevel, posX, up), LevelPosUtil.convert(detailLevel, posZ, up)); } } - + /** * Update the child at the given relative Pos *

* TODO could this be renamed mergeChildData? */ - private void update(byte detailLevel, int posX, int posZ) - { + private void update(byte detailLevel, int posX, int posZ) { posX = LevelPosUtil.getRegionModule(detailLevel, posX); posZ = LevelPosUtil.getRegionModule(detailLevel, posZ); dataContainer[detailLevel].updateData(dataContainer[detailLevel - 1], posX, posZ); } - - + /** * Returns if data exists at the given relative Pos. */ - public boolean doesDataExist(byte detailLevel, int posX, int posZ) - { + public boolean doesDataExist(byte detailLevel, int posX, int posZ) { if (detailLevel < minDetailLevel || dataContainer[detailLevel] == null) return false; - + posX = LevelPosUtil.getRegionModule(detailLevel, posX); posZ = LevelPosUtil.getRegionModule(detailLevel, posZ); - + return dataContainer[detailLevel].doesItExist(posX, posZ); } - + /** * Gets the generation mode for the data point at the given relative pos. */ - public byte getGenerationMode(byte detailLevel, int posX, int posZ) - { + public byte getGenerationMode(byte detailLevel, int posX, int posZ) { if (dataContainer[detailLevel].doesItExist(posX, posZ)) // We take the bottom information always // TODO what does that mean? bottom of what? @@ -400,129 +367,115 @@ public class LodRegion else return DistanceGenerationMode.NONE.complexity; } - + /** - * Returns the lowest (least detailed) detail level in this region - * TODO is that right? + * Returns the lowest (least detailed) detail level in this region TODO is that + * right? */ - public byte getMinDetailLevel() - { + public byte getMinDetailLevel() { return minDetailLevel; } - + /** * Returns the LevelContainer for the detailLevel - * @throws IllegalArgumentException if the detailLevel is less than minDetailLevel + * + * @throws IllegalArgumentException if the detailLevel is less than + * minDetailLevel */ - public LevelContainer getLevel(byte detailLevel) - { + public LevelContainer getLevel(byte detailLevel) { if (detailLevel < minDetailLevel) - throw new IllegalArgumentException("getLevel asked for a detail level that does not exist: minimum: [" + minDetailLevel + "] level requested: [" + detailLevel + "]"); - + throw new IllegalArgumentException("getLevel asked for a detail level that does not exist: minimum: [" + + minDetailLevel + "] level requested: [" + detailLevel + "]"); + return dataContainer[detailLevel]; } - + /** - * Add the levelContainer to this Region, updating the minDetailLevel - * if necessary. - * @throws IllegalArgumentException if the LevelContainer's detailLevel - * is 2 or more detail levels lower than the - * minDetailLevel of this region. + * Add the levelContainer to this Region, updating the minDetailLevel if + * necessary. + * + * @throws IllegalArgumentException if the LevelContainer's detailLevel is 2 or + * more detail levels lower than the + * minDetailLevel of this region. */ - public void addLevelContainer(LevelContainer levelContainer) - { - if (levelContainer.getDetailLevel() < minDetailLevel - 1) - { - throw new IllegalArgumentException( - "the LevelContainer's detailLevel was " - + "[" + levelContainer.getDetailLevel() + "] but this region " - + "only allows adding LevelContainers with a " - + "detail level of [" + (minDetailLevel - 1) + "]"); + public void addLevelContainer(LevelContainer levelContainer) { + if (levelContainer.getDetailLevel() < minDetailLevel - 1) { + throw new IllegalArgumentException("the LevelContainer's detailLevel was " + "[" + + levelContainer.getDetailLevel() + "] but this region " + + "only allows adding LevelContainers with a " + "detail level of [" + (minDetailLevel - 1) + "]"); } - + if (levelContainer.getDetailLevel() == minDetailLevel - 1) minDetailLevel = levelContainer.getDetailLevel(); - + dataContainer[levelContainer.getDetailLevel()] = levelContainer; } - + // TODO James thinks cutTree and growTree (which he renamed to match cutTree) // should have more descriptive names, to make sure the "Tree" portion isn't // confused with Minecraft trees (the plant). - + /** - * Removes any dataContainers that are higher than - * the given detailLevel + * Removes any dataContainers that are higher than the given detailLevel */ - public void cutTree(byte detailLevel) - { - if (detailLevel > minDetailLevel) - { + public void cutTree(byte detailLevel) { + if (detailLevel > minDetailLevel) { for (byte detailLevelIndex = 0; detailLevelIndex < detailLevel; detailLevelIndex++) dataContainer[detailLevelIndex] = null; - + minDetailLevel = detailLevel; } } - + /** - * Make this region more detailed to the detailLevel given. - * TODO is that correct? + * Make this region more detailed to the detailLevel given. TODO is that + * correct? */ - public void growTree(byte detailLevel) - { - if (detailLevel < minDetailLevel) - { - for (byte detailLevelIndex = (byte) (minDetailLevel - 1); detailLevelIndex >= detailLevel; detailLevelIndex--) - { + public void growTree(byte detailLevel) { + if (detailLevel < minDetailLevel) { + for (byte detailLevelIndex = (byte) (minDetailLevel + - 1); detailLevelIndex >= detailLevel; detailLevelIndex--) { if (dataContainer[detailLevelIndex + 1] == null) dataContainer[detailLevelIndex + 1] = new VerticalLevelContainer((byte) (detailLevelIndex + 1)); - + dataContainer[detailLevelIndex] = dataContainer[detailLevelIndex + 1].expand(); } minDetailLevel = detailLevel; } } - + /** * return RegionPos of this lod region */ - public RegionPos getRegionPos() - { + public RegionPos getRegionPos() { return new RegionPos(regionPosX, regionPosZ); } - + /** * Returns how many LODs are in this region */ - public int getNumberOfLods() - { + public int getNumberOfLods() { int count = 0; for (LevelContainer container : dataContainer) count += container.getMaxNumberOfLods(); - + return count; } - - public VerticalQuality getVerticalQuality() - { + + public VerticalQuality getVerticalQuality() { return verticalQuality; } - - public DistanceGenerationMode getGenerationMode() - { + + public DistanceGenerationMode getGenerationMode() { return generationMode; } - - public int getMaxVerticalData(byte detailLevel) - { + + public int getMaxVerticalData(byte detailLevel) { return dataContainer[detailLevel].getVerticalSize(); } - - + @Override - public String toString() - { + public String toString() { return getLevel(LodUtil.REGION_DETAIL_LEVEL).toString(); } } diff --git a/src/main/java/com/seibel/lod/core/objects/opengl/LodVertexBuffer.java b/src/main/java/com/seibel/lod/core/objects/opengl/LodVertexBuffer.java index a0ed97a48..14127a28d 100644 --- a/src/main/java/com/seibel/lod/core/objects/opengl/LodVertexBuffer.java +++ b/src/main/java/com/seibel/lod/core/objects/opengl/LodVertexBuffer.java @@ -56,7 +56,12 @@ public class LodVertexBuffer implements AutoCloseable { if (this.id >= 0) { - GLProxy.getInstance().recordOpenGlCall(() -> GL32.glDeleteBuffers(this.id)); + if (GLProxy.getInstance().getGlContext() == GLProxyContext.PROXY_WORKER) { + GL32.glDeleteBuffers(this.id); + } else { + final int id = this.id; + GLProxy.getInstance().recordOpenGlCall(() -> GL32.glDeleteBuffers(id)); + } this.id = -1; count--; //ClientApi.LOGGER.info("LodVertexBuffer Count: "+count); diff --git a/src/main/java/com/seibel/lod/core/render/GLProxy.java b/src/main/java/com/seibel/lod/core/render/GLProxy.java index 1b9c34f05..99ceb1109 100644 --- a/src/main/java/com/seibel/lod/core/render/GLProxy.java +++ b/src/main/java/com/seibel/lod/core/render/GLProxy.java @@ -25,6 +25,7 @@ import java.io.IOException; import java.io.PrintStream; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import org.lwjgl.glfw.GLFW; import org.lwjgl.opengl.GL; @@ -39,6 +40,7 @@ import com.seibel.lod.core.api.ClientApi; import com.seibel.lod.core.enums.config.GpuUploadMethod; import com.seibel.lod.core.enums.rendering.DebugMode; import com.seibel.lod.core.enums.rendering.GLProxyContext; +import com.seibel.lod.core.util.LodThreadFactory; import com.seibel.lod.core.util.SingletonHandler; import com.seibel.lod.core.wrapperInterfaces.config.ILodConfigWrapperSingleton; import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftWrapper; @@ -65,7 +67,7 @@ public class GLProxy private static final IMinecraftWrapper MC = SingletonHandler.get(IMinecraftWrapper.class); - private static final ExecutorService workerThread = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat(GLProxy.class.getSimpleName() + "-Worker-Thread").build()); + private ExecutorService workerThread = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat(GLProxy.class.getSimpleName() + "-Worker-Thread").build()); private static final ILodConfigWrapperSingleton CONFIG = SingletonHandler.get(ILodConfigWrapperSingleton.class); @@ -423,6 +425,22 @@ public class GLProxy } } + public static void ensureAllGLJobCompleted() { + if (!hasInstance()) return; + ClientApi.LOGGER.info("Blocking until GL jobs finished!"); + try { + instance.workerThread.shutdown(); + boolean worked = instance.workerThread.awaitTermination(30, TimeUnit.SECONDS); + if (!worked) + ClientApi.LOGGER.error("GLWorkerThread shutdown timed out! Game may crash on exit due to cleanup failure!"); + } catch (InterruptedException e) { + ClientApi.LOGGER.error("GLWorkerThread shutdown is interrupted! Game may crash on exit due to cleanup failure!"); + e.printStackTrace(); + } finally { + instance.workerThread = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat(GLProxy.class.getSimpleName() + "-Worker-Thread").build()); + } + } + /** * If called from a legacy OpenGL context this will * set the fog end to infinity with a density of 0. From a8d52c10595eecd04c117a3e3dad15d8b5f918d2 Mon Sep 17 00:00:00 2001 From: coolGi2007 Date: Thu, 6 Jan 2022 09:58:00 +0000 Subject: [PATCH 10/20] Updated lang --- .../config/ILodConfigWrapperSingleton.java | 9 ++++++++- src/main/resources/assets/lod/lang/en_us.json | 6 ++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/seibel/lod/core/wrapperInterfaces/config/ILodConfigWrapperSingleton.java b/src/main/java/com/seibel/lod/core/wrapperInterfaces/config/ILodConfigWrapperSingleton.java index 5f4f335c1..8b2d8092a 100644 --- a/src/main/java/com/seibel/lod/core/wrapperInterfaces/config/ILodConfigWrapperSingleton.java +++ b/src/main/java/com/seibel/lod/core/wrapperInterfaces/config/ILodConfigWrapperSingleton.java @@ -54,6 +54,13 @@ public interface ILodConfigWrapperSingleton IGraphics graphics(); IWorldGenerator worldGenerator(); IAdvanced advanced(); + + + boolean OPTIONS_BUTTON_DEFAULT = true; + String OPTIONS_BUTTON_DESC = "" + + " Show the lod button in the options screen next to fov"; + boolean getOptionsButton(); + void setOptionsButton(boolean newOptionsButton); //==================// @@ -274,7 +281,7 @@ public interface ILodConfigWrapperSingleton + " Higher settings will make terrain look good when looking backwards \n" + " when changing speeds quickly, but will increase upload times and GPU usage."; int getBacksideCullingRange(); - void setBacksideCullingRange(int backsideCullingRange); + void setBacksideCullingRange(int newBacksideCullingRange); boolean USE_EXTENDED_NEAR_CLIP_PLANE_DEFAULT = false; String USE_EXTENDED_NEAR_CLIP_PLANE_DESC = "" diff --git a/src/main/resources/assets/lod/lang/en_us.json b/src/main/resources/assets/lod/lang/en_us.json index 51724f6d2..9bc8fcdc1 100644 --- a/src/main/resources/assets/lod/lang/en_us.json +++ b/src/main/resources/assets/lod/lang/en_us.json @@ -1,8 +1,8 @@ { "lod.title": "Distant Horizons", "DistantHorizons.config.title": "Distant Horizons config", - "DistantHorizons.config.ShowButton": "Show menu button", - "DistantHorizons.config.ShowButton.@tooltip": "Show the custom button to the left of the fov button", + "DistantHorizons.config.optionsButton": "Show options button", + "DistantHorizons.config.optionsButton.@tooltip": "Show the custom button to the left of the fov button", "DistantHorizons.config.client": "Client", "DistantHorizons.config.client.graphics": "Graphics", "DistantHorizons.config.client.graphics.quality": "Quality options", @@ -120,6 +120,8 @@ "DistantHorizons.config.enum.DebugMode.OFF": "Off", "DistantHorizons.config.enum.DebugMode.SHOW_DETAIL": "Show detail", "DistantHorizons.config.enum.DebugMode.SHOW_DETAIL_WIREFRAME": "Show detail with wireframe", + "DistantHorizons.config.enum.DebugMode.SHOW_GENMODE": "Show generation mode", + "DistantHorizons.config.enum.DebugMode.SHOW_GENMODE_WIREFRAME": "Show generation mode with wireframe", "DistantHorizons.config.enum.GpuUploadMethod.AUTO": "Auto", "DistantHorizons.config.enum.GpuUploadMethod.BUFFER_STORAGE": "Buffer storage", "DistantHorizons.config.enum.GpuUploadMethod.SUB_DATA": "Sub data", From 5ac51dd2a5fd24be6203d22004a760ad9d696fa6 Mon Sep 17 00:00:00 2001 From: tom lee Date: Fri, 7 Jan 2022 13:28:12 +0800 Subject: [PATCH 11/20] Fixed accidentally using java 9+ features. Now java 7 should work --- .../lod/core/handlers/LodDimensionFileHandler.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHandler.java b/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHandler.java index a2ddc873a..9f943430c 100644 --- a/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHandler.java +++ b/src/main/java/com/seibel/lod/core/handlers/LodDimensionFileHandler.java @@ -264,7 +264,10 @@ public class LodDimensionFileHandler if (regionToSave.isEmpty()) return; // Use Memory order Acquire to acquire any memory changes on getting this boolean // (Corresponding call is the this::writerMain(...)::...setRelease(false);) - boolean haventStarted = !isFileWritingThreadRunning.compareAndExchangeAcquire(false, true); + //boolean haventStarted = !isFileWritingThreadRunning.compareAndExchangeAcquire(false, true); + // The above needs java 9! + boolean haventStarted = !isFileWritingThreadRunning.compareAndSet(false, true); + if (haventStarted) { // We acquired the atomic lock. fileWritingThreadPool.execute(this::writerMain); @@ -274,7 +277,10 @@ public class LodDimensionFileHandler private void writerMain() { // Use Memory order Relaxed as no additional memory changes needed to be visible. // (This is just a safety checks) - boolean isStarted = isFileWritingThreadRunning.getPlain(); + // boolean isStarted = isFileWritingThreadRunning.getPlain(); + // The above needs java 9! + boolean isStarted = isFileWritingThreadRunning.get(); + if (!isStarted) throw new ConcurrentModificationException("WriterMain Triggered but the thead state is not started!?"); ClientApi.LOGGER.info("Lod File Writer started. To-be-written-regions: "+regionToSave.size()); Instant start = Instant.now(); @@ -303,7 +309,9 @@ public class LodDimensionFileHandler ClientApi.LOGGER.info("Lod File Writer completed. Took "+Duration.between(start, end)); // Use Memory order Release to release any memory changes on setting this boolean // (Corresponding call is the this::saveRegions(...)::...compareAndExchangeAcquire(false, true);) - isFileWritingThreadRunning.setRelease(false); + // isFileWritingThreadRunning.setRelease(false); + // The above needs java 9! + isFileWritingThreadRunning.set(false); } /** From cba75123c75f38d74c8dd81bafafd81f88ea524f Mon Sep 17 00:00:00 2001 From: tom lee Date: Fri, 7 Jan 2022 15:50:42 +0800 Subject: [PATCH 12/20] Fixed Overdraw, mergeVertData error, wrong vanillaDist math. --- .../com/seibel/lod/core/api/EventApi.java | 2 +- .../LodBufferBuilderFactory.java | 54 +++++-------- .../core/builders/lodBuilding/LodBuilder.java | 14 ++-- .../handlers/LodDimensionFileHandler.java | 2 +- .../lod/core/objects/VertexOptimizer.java | 1 + .../objects/lod/VerticalLevelContainer.java | 8 +- .../seibel/lod/core/render/LodRenderer.java | 80 +++++++------------ .../com/seibel/lod/core/util/LodUtil.java | 12 +++ .../seibel/lod/core/util/MovableGridList.java | 26 +++++- .../minecraft/IMinecraftRenderWrapper.java | 7 +- 10 files changed, 103 insertions(+), 103 deletions(-) diff --git a/src/main/java/com/seibel/lod/core/api/EventApi.java b/src/main/java/com/seibel/lod/core/api/EventApi.java index ca4f48623..57e831a2e 100644 --- a/src/main/java/com/seibel/lod/core/api/EventApi.java +++ b/src/main/java/com/seibel/lod/core/api/EventApi.java @@ -96,7 +96,7 @@ public class EventApi public void chunkLoadEvent(IChunkWrapper chunk, IDimensionTypeWrapper dimType) { - ApiShared.lodBuilder.generateLodNodeAsync(chunk, ApiShared.lodWorld, dimType, DistanceGenerationMode.FULL); + ApiShared.lodBuilder.generateLodNodeAsync(chunk, ApiShared.lodWorld, dimType, DistanceGenerationMode.FULL, true); } public void worldSaveEvent() diff --git a/src/main/java/com/seibel/lod/core/builders/bufferBuilding/LodBufferBuilderFactory.java b/src/main/java/com/seibel/lod/core/builders/bufferBuilding/LodBufferBuilderFactory.java index bca3a0bbe..646672558 100644 --- a/src/main/java/com/seibel/lod/core/builders/bufferBuilding/LodBufferBuilderFactory.java +++ b/src/main/java/com/seibel/lod/core/builders/bufferBuilding/LodBufferBuilderFactory.java @@ -461,21 +461,22 @@ public class LodBufferBuilderFactory int chunkXdist = LevelPosUtil.getChunkPos(detailLevel, posX) - playerChunkX; int chunkZdist = LevelPosUtil.getChunkPos(detailLevel, posZ) - playerChunkZ; - // Currently fixing below - // FIXME: We don't need to ignore rendered chunks! Just build it and leave it for the renderer to decide! - //We don't want to render this fake block if - //The block is inside the render distance with, is not bigger than a chunk and is positioned in a chunk set as vanilla rendered - // - //The block is in the player chunk or in a chunk adjacent to the player - //if(isThisPositionGoingToBeRendered(detailLevel, posX, posZ, playerChunkX, playerChunkZ, vanillaRenderedChunks, gameChunkRenderDistance)) - //{ - // continue; - //} + // TODO: In the future, We don't need to ignore rendered chunks! Just build it and leave it for the renderer to decide! + // We don't want to render this fake block if + // The block is inside the render distance with, is not bigger than a chunk and is positioned in a chunk set as vanilla rendered + + // The block is in the player chunk or in a chunk adjacent to the player + if(detailLevel <= LodUtil.CHUNK_DETAIL_LEVEL && + isThisPositionGoingToBeRendered(LevelPosUtil.getChunkPos(detailLevel, posX), + LevelPosUtil.getChunkPos(detailLevel, posZ))) + { + continue; + } //we check if the block to render is not in player chunk boolean posNotInPlayerChunk = !(chunkXdist == 0 && chunkZdist == 0); - - // We extract the adj data in the four cardinal direction + + // We extract the adj data in the four cardinal direction // we first reset the adjShadeDisabled. This is used to disable the shade on the border when we have transparent block like water or glass // to avoid having a "darker border" underground @@ -558,31 +559,20 @@ public class LodBufferBuilderFactory return true; } - - + // Will be removed in a1.7 @Deprecated - private boolean isThisPositionGoingToBeRendered(byte detailLevel, int posX, int posZ, int chunkPosX, int chunkPosZ, boolean[][] vanillaRenderedChunks, int gameChunkRenderDistance){ + private boolean isThisPositionGoingToBeRendered(int chunkX, int chunkZ){ + MovableGridList chunkGrid = ClientApi.renderer.vanillaRenderedChunks; + Boolean isRendered = chunkGrid.get(chunkX, chunkZ); + // skip any chunks that Minecraft is going to render - int chunkXdist = LevelPosUtil.getChunkPos(detailLevel, posX) - chunkPosX; - int chunkZdist = LevelPosUtil.getChunkPos(detailLevel, posZ) - chunkPosZ; - + if (isRendered == null || !isRendered) return false; + // check if the chunk is on the border - boolean isItBorderPos; if (CONFIG.client().graphics().advancedGraphics().getVanillaOverdraw() == VanillaOverdraw.BORDER) - isItBorderPos = LodUtil.isBorderChunk(vanillaRenderedChunks, chunkXdist + gameChunkRenderDistance + 1, chunkZdist + gameChunkRenderDistance + 1); + return !LodUtil.isBorderChunk(ClientApi.renderer.vanillaRenderedChunks, chunkX, chunkZ); else - isItBorderPos = false; - - - //boolean smallRenderDistance = gameChunkRenderDistance <= LodUtil.MINIMUM_RENDER_DISTANCE_FOR_PARTIAL_OVERDRAW; - - // get the positions that will be rendered - - return (gameChunkRenderDistance >= Math.abs(chunkXdist) - && gameChunkRenderDistance >= Math.abs(chunkZdist) - && detailLevel <= LodUtil.CHUNK_DETAIL_LEVEL - && vanillaRenderedChunks[chunkXdist + gameChunkRenderDistance + 1][chunkZdist + gameChunkRenderDistance + 1]) - && (!isItBorderPos); + return true; } diff --git a/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java b/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java index d26c86ad0..8af36d0d7 100644 --- a/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java +++ b/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java @@ -90,10 +90,10 @@ public class LodBuilder public void generateLodNodeAsync(IChunkWrapper chunk, LodWorld lodWorld, IDimensionTypeWrapper dim) { - generateLodNodeAsync(chunk, lodWorld, dim, DistanceGenerationMode.FULL); + generateLodNodeAsync(chunk, lodWorld, dim, DistanceGenerationMode.FULL, false); } - public void generateLodNodeAsync(IChunkWrapper chunk, LodWorld lodWorld, IDimensionTypeWrapper dim, DistanceGenerationMode generationMode) + public void generateLodNodeAsync(IChunkWrapper chunk, LodWorld lodWorld, IDimensionTypeWrapper dim, DistanceGenerationMode generationMode, boolean override) { if (lodWorld == null || lodWorld.getIsWorldNotLoaded()) return; @@ -130,7 +130,7 @@ public class LodBuilder { lodDim = lodWorld.getLodDimension(dim); } - generateLodNodeFromChunk(lodDim, chunk, new LodBuilderConfig(generationMode)); + generateLodNodeFromChunk(lodDim, chunk, new LodBuilderConfig(generationMode), override); //} //catch (IllegalArgumentException | NullPointerException e) //{ @@ -149,14 +149,14 @@ public class LodBuilder */ public void generateLodNodeFromChunk(LodDimension lodDim, IChunkWrapper chunk) throws IllegalArgumentException { - generateLodNodeFromChunk(lodDim, chunk, new LodBuilderConfig()); + generateLodNodeFromChunk(lodDim, chunk, new LodBuilderConfig(), false); } /** * Creates a LodNode for a chunk in the given world. * @throws IllegalArgumentException thrown if either the chunk or world is null. */ - public void generateLodNodeFromChunk(LodDimension lodDim, IChunkWrapper chunk, LodBuilderConfig config) + public void generateLodNodeFromChunk(LodDimension lodDim, IChunkWrapper chunk, LodBuilderConfig config, boolean override) throws IllegalArgumentException { //long executeTime = System.currentTimeMillis(); @@ -204,7 +204,9 @@ public class LodBuilder { posX = LevelPosUtil.convert((byte) 0, chunk.getChunkPosX() * 16 + startX, minDetailLevel); posZ = LevelPosUtil.convert((byte) 0, chunk.getChunkPosZ() * 16 + startZ, minDetailLevel); - if (!lodDim.doesDataExist(minDetailLevel, posX, posZ)) { + long oldData = lodDim.getSingleData(minDetailLevel, posX, posZ); + if (override || !DataPointUtil.doesItExist(oldData) || + DataPointUtil.getGenerationMode(oldData) DIRECTION_NORMAL_MAP = new HashMap() {{ diff --git a/src/main/java/com/seibel/lod/core/objects/lod/VerticalLevelContainer.java b/src/main/java/com/seibel/lod/core/objects/lod/VerticalLevelContainer.java index d0a280a49..1e580769b 100644 --- a/src/main/java/com/seibel/lod/core/objects/lod/VerticalLevelContainer.java +++ b/src/main/java/com/seibel/lod/core/objects/lod/VerticalLevelContainer.java @@ -210,14 +210,14 @@ public class VerticalLevelContainer implements LevelContainer } private static long[] downgradeVerticalSize(int oldVertSize, int newVertSize, long[] data) { - long[] dataToMerge = new long[oldVertSize]; int size = data.length/oldVertSize; + long[] dataToMerge = new long[oldVertSize]; long[] newData = new long[size * newVertSize]; for (int i = 0; i < size; i++) { - System.arraycopy(oldVertSize, i * oldVertSize, dataToMerge, 0, oldVertSize); - dataToMerge = DataPointUtil.mergeMultiData(dataToMerge, oldVertSize, newVertSize); - System.arraycopy(dataToMerge, 0, newData, i * newVertSize, newVertSize); + System.arraycopy(data, i * oldVertSize, dataToMerge, 0, oldVertSize); + long[] tempBuffer = DataPointUtil.mergeMultiData(dataToMerge, oldVertSize, newVertSize); + System.arraycopy(tempBuffer, 0, newData, i * newVertSize, newVertSize); } return newData; } diff --git a/src/main/java/com/seibel/lod/core/render/LodRenderer.java b/src/main/java/com/seibel/lod/core/render/LodRenderer.java index c78d41c06..b80ed2bac 100644 --- a/src/main/java/com/seibel/lod/core/render/LodRenderer.java +++ b/src/main/java/com/seibel/lod/core/render/LodRenderer.java @@ -59,21 +59,6 @@ import com.seibel.lod.core.wrapperInterfaces.minecraft.IProfilerWrapper; */ public class LodRenderer { - public static class VanillaRenderedChunksList extends GridList { - private static final long serialVersionUID = -5448501880911391315L; - - public final int centerX; - public final int centerZ; - - public VanillaRenderedChunksList(int range, int centerX, int centerZ) { - super(range); - this.centerX = centerX; - this.centerZ = centerZ; - for (int i=0; i vanillaRenderedChunks; public int vanillaRenderedChunksCenterX; public int vanillaRenderedChunksCenterZ; public int vanillaRenderedChunksRefreshTimer; @@ -504,50 +489,43 @@ public class LodRenderer // returns whether anything changed private boolean updateVanillaRenderedChunks(LodDimension lodDim, boolean recreateChunks) { short chunkRenderDistance = (short) MC_RENDER.getRenderDistance(); - int chunkX = Math.floorDiv(previousPos.getX(), 16); - int chunkZ = Math.floorDiv(previousPos.getZ(), 16); + int chunkX = Math.floorDiv(lastUpdatedPos.getX(), 16); + int chunkZ = Math.floorDiv(lastUpdatedPos.getZ(), 16); // if the player is high enough, draw all LODs - if (previousPos.getY() > 256) { - vanillaRenderedChunks = new VanillaRenderedChunksList( + if (lastUpdatedPos.getY() > 256) { + vanillaRenderedChunks = new MovableGridList( chunkRenderDistance, chunkX, chunkZ); return true; } - VanillaRenderedChunksList chunkList; - - if (recreateChunks) { - vanillaRenderedChunks = new VanillaRenderedChunksList(chunkRenderDistance, chunkX, chunkZ); - return true; - } else { - chunkList = vanillaRenderedChunks; - chunkX = chunkList.centerX; - chunkZ = chunkList.centerZ; - chunkRenderDistance = (short) vanillaRenderedChunks.gridCentreToEdge; - } + MovableGridList chunkList; boolean anyChanged = false; + if (recreateChunks || vanillaRenderedChunks.gridCentreToEdge != chunkRenderDistance) { + chunkList = new MovableGridList(chunkRenderDistance, chunkX, chunkZ); + anyChanged = true; + } else { + // anyChanged = vanillaRenderedChunks.move(chunkX, chunkZ); + // chunkList = vanillaRenderedChunks; + chunkList = new MovableGridList(chunkRenderDistance, chunkX, chunkZ); + anyChanged = true; + } + LagSpikeCatcher getChunks = new LagSpikeCatcher(); - Set chunkPosToSkip = LodUtil.getNearbyLodChunkPosToSkip(lodDim, previousPos); + Set chunkPosToSkip = LodUtil.getNearbyLodChunkPosToSkip(lodDim, lastUpdatedPos); getChunks.end("LodDrawSetup:UpdateStatus:UpdateVanillaChunks:getChunks"); for (AbstractChunkPosWrapper pos : chunkPosToSkip) { - int xIndex = (pos.getX() - chunkX) + (chunkRenderDistance + 1); - int zIndex = (pos.getZ() - chunkZ) + (chunkRenderDistance + 1); - // sometimes we are given chunks that are outside the render distance, // This prevents index out of bounds exceptions - if (xIndex >= 0 && zIndex >= 0 - && xIndex < vanillaRenderedChunks.gridSize - && zIndex < vanillaRenderedChunks.gridSize) + if (!chunkList.inRange(pos.getX(), pos.getZ())) continue; + Boolean oldBool = chunkList.swap(pos.getX(), pos.getZ(), true); + if (oldBool == null || !oldBool) { - if (!chunkList.get(chunkList.calculateOffset(xIndex, zIndex))) - { - chunkList.set(chunkList.calculateOffset(xIndex, zIndex), true); - anyChanged = true; - lodDim.markRegionBufferToRegen(pos.getRegionX(), pos.getRegionZ()); - } + anyChanged = true; + lodDim.markRegionBufferToRegen(pos.getRegionX(), pos.getRegionZ()); } } - vanillaRenderedChunks = chunkList; + if (anyChanged) vanillaRenderedChunks = chunkList; return anyChanged; } @@ -577,12 +555,12 @@ public class LodRenderer // check if the player has moved if (newTime - prevPlayerPosTime > CONFIG.client().advanced().buffers().getRebuildTimes().playerMoveTimeout) { - if (previousPos == null - || Math.abs(newPos.getX() - previousPos.getX()) > CONFIG.client().advanced().buffers().getRebuildTimes().playerMoveDistance*16 - || Math.abs(newPos.getZ() - previousPos.getZ()) > CONFIG.client().advanced().buffers().getRebuildTimes().playerMoveDistance*16) + if (lastUpdatedPos == null + || Math.abs(newPos.getX() - lastUpdatedPos.getX()) > CONFIG.client().advanced().buffers().getRebuildTimes().playerMoveDistance*16 + || Math.abs(newPos.getZ() - lastUpdatedPos.getZ()) > CONFIG.client().advanced().buffers().getRebuildTimes().playerMoveDistance*16) { tryPartialGen = true; - previousPos = newPos; + lastUpdatedPos = newPos; posUpdated = true; } prevPlayerPosTime = newTime; @@ -604,7 +582,7 @@ public class LodRenderer if (tryFullGen && !posUpdated) { - previousPos = newPos; + lastUpdatedPos = newPos; posUpdated = true; } shouldUpdateChunks |= posUpdated; diff --git a/src/main/java/com/seibel/lod/core/util/LodUtil.java b/src/main/java/com/seibel/lod/core/util/LodUtil.java index d14b41932..f301aa520 100644 --- a/src/main/java/com/seibel/lod/core/util/LodUtil.java +++ b/src/main/java/com/seibel/lod/core/util/LodUtil.java @@ -394,6 +394,7 @@ public class LodUtil * @param z relative (to the matrix) z chunk to check * @return true if and only if the chunk is a border of the renderable chunks */ + @Deprecated public static boolean isBorderChunk(boolean[][] vanillaRenderedChunks, int x, int z) { if (x < 0 || z < 0 || x >= vanillaRenderedChunks.length || z >= vanillaRenderedChunks[0].length) @@ -410,6 +411,17 @@ public class LodUtil } return false; } + public static boolean isBorderChunk(MovableGridList vanillaRenderedChunks, int chunkX, int chunkZ) + { + for (LodDirection lodDirection : VertexOptimizer.ADJ_DIRECTIONS) + { + int tempX = chunkX + lodDirection.getNormal().x; + int tempZ = chunkZ + lodDirection.getNormal().z; + Boolean b = vanillaRenderedChunks.get(tempX, tempZ); + if (b == null || !b) return true; + } + return false; + } /** This is copied from Minecraft's MathHelper class */ diff --git a/src/main/java/com/seibel/lod/core/util/MovableGridList.java b/src/main/java/com/seibel/lod/core/util/MovableGridList.java index acdb4c67d..d620321da 100644 --- a/src/main/java/com/seibel/lod/core/util/MovableGridList.java +++ b/src/main/java/com/seibel/lod/core/util/MovableGridList.java @@ -67,11 +67,25 @@ public class MovableGridList extends ArrayList implements List { } // return null if x,y is outside of the grid + // Otherwise, return the new value (for chaining) public T setAndGet(int x, int y, T t) { x = x-centerX+gridCentreToEdge; y = y-centerY+gridCentreToEdge; return _setDirect(x,y, t) ? t : null; } + // return null if x,y is outside of the grid + // Otherwise, return the old value + public T swap(int x, int y, T t) { + x = x-centerX+gridCentreToEdge; + y = y-centerY+gridCentreToEdge; + return _swapDirect(x,y, t); + } + + public boolean inRange(int x, int y) { + x = x-centerX+gridCentreToEdge; + y = y-centerY+gridCentreToEdge; + return (x>=0 && x=0 && y=gridSize || y<0 || y>=gridSize) return null; @@ -82,9 +96,14 @@ public class MovableGridList extends ArrayList implements List { super.set(x + y * gridSize, t); return true; } + private final T _swapDirect(int x, int y, T t) { + if (x<0 || x>=gridSize || y<0 || y>=gridSize) return null; + return super.set(x + y * gridSize, t); + } - public void move(int newCenterX, int newCenterY) { - if (centerX == newCenterX && centerY == newCenterY) return; + // Return false if haven't changed. Return true if it did + public boolean move(int newCenterX, int newCenterY) { + if (centerX == newCenterX && centerY == newCenterY) return false; int deltaX = newCenterX - centerX; int deltaY = newCenterY - centerY; @@ -97,7 +116,7 @@ public class MovableGridList extends ArrayList implements List { // update the new center centerX = newCenterX; centerY = newCenterY; - return; + return true; } centerX = newCenterX; centerY = newCenterY; @@ -147,6 +166,7 @@ public class MovableGridList extends ArrayList implements List { } } } + return true; } public void move(int newCenterX, int newCenterY, Consumer d) { diff --git a/src/main/java/com/seibel/lod/core/wrapperInterfaces/minecraft/IMinecraftRenderWrapper.java b/src/main/java/com/seibel/lod/core/wrapperInterfaces/minecraft/IMinecraftRenderWrapper.java index 886e9603a..03722b04b 100644 --- a/src/main/java/com/seibel/lod/core/wrapperInterfaces/minecraft/IMinecraftRenderWrapper.java +++ b/src/main/java/com/seibel/lod/core/wrapperInterfaces/minecraft/IMinecraftRenderWrapper.java @@ -101,9 +101,6 @@ public interface IMinecraftRenderWrapper IWrapperFactory factory = SingletonHandler.get(IWrapperFactory.class); int chunkRenderDist = this.getRenderDistance(); - // if we have a odd render distance, we'll have a empty gap. This way we'll overlap by 1 instead, - // which is preferable to having a hole in the world - chunkRenderDist = chunkRenderDist % 2 == 0 ? chunkRenderDist : chunkRenderDist - 1; AbstractChunkPosWrapper centerChunkPos = mcWrapper.getPlayerChunkPos(); int startChunkX = centerChunkPos.getX() - chunkRenderDist; @@ -111,9 +108,9 @@ public interface IMinecraftRenderWrapper // add every position within render distance HashSet renderedPos = new HashSet(); - for (int chunkX = 0; chunkX < (chunkRenderDist * 2); chunkX++) + for (int chunkX = 0; chunkX < (chunkRenderDist * 2+1); chunkX++) { - for(int chunkZ = 0; chunkZ < (chunkRenderDist * 2); chunkZ++) + for(int chunkZ = 0; chunkZ < (chunkRenderDist * 2+1); chunkZ++) { renderedPos.add(factory.createChunkPos(startChunkX + chunkX, startChunkZ + chunkZ)); } From d4123a44ed2976451178e0510d00a1880a7a87ee Mon Sep 17 00:00:00 2001 From: tom lee Date: Fri, 7 Jan 2022 18:58:29 +0800 Subject: [PATCH 13/20] Add DropoffQuality --- .../lod/core/enums/config/DropoffQuality.java | 48 ++++++++++++++++ .../lod/core/objects/lod/LodDimension.java | 18 ++++-- .../lod/core/objects/lod/LodRegion.java | 55 +++++++++++++++++-- .../config/ILodConfigWrapperSingleton.java | 16 ++++++ 4 files changed, 129 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/seibel/lod/core/enums/config/DropoffQuality.java diff --git a/src/main/java/com/seibel/lod/core/enums/config/DropoffQuality.java b/src/main/java/com/seibel/lod/core/enums/config/DropoffQuality.java new file mode 100644 index 000000000..271c8c68b --- /dev/null +++ b/src/main/java/com/seibel/lod/core/enums/config/DropoffQuality.java @@ -0,0 +1,48 @@ +/* + * This file is part of the Distant Horizon mod (formerly the LOD Mod), + * licensed under the GNU GPL v3 License. + * + * Copyright (C) 2022 Tom Lee (TomTheFurry) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.seibel.lod.core.enums.config; + +/** + * AUTO
+ * SMOOTH_DROPOFF
+ * PERFORMANCE_FOCUSED
+ *
+ * Determines how lod level drop off should be done + * + * @author Tom Lee + * @version 7-1-2022 + */ +public enum DropoffQuality { + + /** SMOOTH_DROPOFF when <128 lod view distance, or PERFORMANCE_FOCUSED otherwise */ + AUTO(-1), + + SMOOTH_DROPOFF(10), + + PERFORMANCE_FOCUSED(0); + + public final int fastModeSwitch; + + DropoffQuality(int fastModeSwitch) { + this.fastModeSwitch = fastModeSwitch; + } + + +} diff --git a/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java b/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java index b249e95fe..3b59af3ce 100644 --- a/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java +++ b/src/main/java/com/seibel/lod/core/objects/lod/LodDimension.java @@ -26,6 +26,7 @@ import java.util.concurrent.Executors; import com.seibel.lod.core.api.ClientApi; import com.seibel.lod.core.enums.config.DistanceGenerationMode; +import com.seibel.lod.core.enums.config.DropoffQuality; import com.seibel.lod.core.enums.config.GenerationPriority; import com.seibel.lod.core.enums.config.VerticalQuality; import com.seibel.lod.core.handlers.LodDimensionFileHandler; @@ -385,7 +386,11 @@ public class LodDimension DistanceGenerationMode generationMode = CONFIG.client().worldGenerator().getDistanceGenerationMode(); VerticalQuality verticalQuality = CONFIG.client().graphics().quality().getVerticalQuality(); - + DropoffQuality dropoffQuality = CONFIG.client().graphics().quality().getDropoffQuality(); + if (dropoffQuality == DropoffQuality.AUTO) + dropoffQuality = CONFIG.client().graphics().quality().getLodChunkRenderDistance() < 128 ? + DropoffQuality.SMOOTH_DROPOFF : DropoffQuality.PERFORMANCE_FOCUSED; + int dropoffSwitch = dropoffQuality.fastModeSwitch; // don't run the expander multiple times // for the same location Runnable thread = () -> { @@ -423,10 +428,10 @@ public class LodDimension region = getRegionFromFile(regions[x][z], minDetail, generationMode, verticalQuality); regions[x][z] = region; updated = true; - } else if (region.lastMaxDetailLevel != maxDetail) { + } else if (minDetail <= dropoffSwitch && region.lastMaxDetailLevel != maxDetail) { region.lastMaxDetailLevel = maxDetail; updated = true; - } else if (region.lastMaxDetailLevel != region.getMinDetailLevel()) { + } else if (minDetail <= dropoffSwitch && region.lastMaxDetailLevel != region.getMinDetailLevel()) { updated = true; } if (updated) { @@ -503,11 +508,16 @@ public class LodDimension GenerationPriority generationPriority = CONFIG.client().worldGenerator().getGenerationPriority(); if (generationPriority == GenerationPriority.AUTO) generationPriority = MC.hasSinglePlayerServer() ? GenerationPriority.FAR_FIRST : GenerationPriority.NEAR_FIRST; + + DropoffQuality dropoffQuality = CONFIG.client().graphics().quality().getDropoffQuality(); + if (dropoffQuality == DropoffQuality.AUTO) + dropoffQuality = CONFIG.client().graphics().quality().getLodChunkRenderDistance() < 128 ? + DropoffQuality.SMOOTH_DROPOFF : DropoffQuality.PERFORMANCE_FOCUSED; boolean requireCorrectDetailLevel = generationPriority == GenerationPriority.NEAR_FIRST; if (region != null) - region.getPosToRender(posToRender, playerPosX, playerPosZ, requireCorrectDetailLevel); + region.getPosToRender(posToRender, playerPosX, playerPosZ, requireCorrectDetailLevel, dropoffQuality); } /** diff --git a/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java b/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java index 936d16d21..b5dc6a9aa 100644 --- a/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java +++ b/src/main/java/com/seibel/lod/core/objects/lod/LodRegion.java @@ -20,6 +20,7 @@ package com.seibel.lod.core.objects.lod; import com.seibel.lod.core.enums.config.DistanceGenerationMode; +import com.seibel.lod.core.enums.config.DropoffQuality; import com.seibel.lod.core.enums.config.GenerationPriority; import com.seibel.lod.core.enums.config.VerticalQuality; import com.seibel.lod.core.objects.PosToGenerateContainer; @@ -192,7 +193,6 @@ public class LodRegion { int childPosX = childOffsetPosX * 2; int childPosZ = childOffsetPosZ * 2; - int childSize = 1 << (LodUtil.REGION_DETAIL_LEVEL - childDetailLevel); byte targetDetailLevel = DetailDistanceUtil.getGenerationDetailFromDistance(minDistance); if (targetDetailLevel <= detailLevel) { @@ -233,9 +233,15 @@ public class LodRegion { * understand */ public void getPosToRender(PosToRenderContainer posToRender, int playerPosX, int playerPosZ, - boolean requireCorrectDetailLevel) { - getPosToRender(posToRender, LodUtil.REGION_DETAIL_LEVEL, 0, 0, playerPosX, playerPosZ, - requireCorrectDetailLevel); + boolean requireCorrectDetailLevel, DropoffQuality dropoffQuality) { + int minDistance = LevelPosUtil.minDistance(LodUtil.REGION_DETAIL_LEVEL, 0, 0, playerPosX, playerPosZ, regionPosX, regionPosZ); + byte targetLevel = DetailDistanceUtil.getDrawDetailFromDistance(minDistance); + if (targetLevel <= dropoffQuality.fastModeSwitch) { + getPosToRender(posToRender, LodUtil.REGION_DETAIL_LEVEL, 0, 0, playerPosX, playerPosZ, + requireCorrectDetailLevel); + } else { + getPosToRenderFlat(posToRender, LodUtil.REGION_DETAIL_LEVEL, 0, 0, targetLevel, requireCorrectDetailLevel); + } } /** @@ -305,6 +311,47 @@ public class LodRegion { } } + /** + * This method will fill the posToRender array with all levelPos that are + * render-able. But the entire region try use the same detail level. + */ + private void getPosToRenderFlat(PosToRenderContainer posToRender, byte detailLevel, int posX, int posZ, byte targetLevel, boolean requireCorrectDetailLevel) { + // equivalent to 2^(...) + int size = 1 << (LodUtil.REGION_DETAIL_LEVEL - detailLevel); + + if (detailLevel == targetLevel) { + posToRender.addPosToRender(detailLevel, posX + regionPosX * size, posZ + regionPosZ * size); + } else // case where (detailLevel > desiredLevel) + { + int childPosX = posX * 2; + int childPosZ = posZ * 2; + byte childDetailLevel = (byte) (detailLevel - 1); + int childrenCount = 0; + + for (int x = 0; x <= 1; x++) { + for (int z = 0; z <= 1; z++) { + if (doesDataExist(childDetailLevel, childPosX + x, childPosZ + z)) { + if (!requireCorrectDetailLevel) + childrenCount++; + else + getPosToRenderFlat(posToRender, childDetailLevel, childPosX + x, childPosZ + z, targetLevel, requireCorrectDetailLevel); + } + } + } + + if (!requireCorrectDetailLevel) { + // If all the four children exist go deeper + if (childrenCount == 4) { + for (int x = 0; x <= 1; x++) + for (int z = 0; z <= 1; z++) + getPosToRenderFlat(posToRender, childDetailLevel, childPosX + x, childPosZ + z, targetLevel, requireCorrectDetailLevel); + } else { + posToRender.addPosToRender(detailLevel, posX + regionPosX * size, posZ + regionPosZ * size); + } + } + } + } + /** * Updates all children. *

diff --git a/src/main/java/com/seibel/lod/core/wrapperInterfaces/config/ILodConfigWrapperSingleton.java b/src/main/java/com/seibel/lod/core/wrapperInterfaces/config/ILodConfigWrapperSingleton.java index 8b2d8092a..00da18d5c 100644 --- a/src/main/java/com/seibel/lod/core/wrapperInterfaces/config/ILodConfigWrapperSingleton.java +++ b/src/main/java/com/seibel/lod/core/wrapperInterfaces/config/ILodConfigWrapperSingleton.java @@ -22,6 +22,7 @@ package com.seibel.lod.core.wrapperInterfaces.config; import com.seibel.lod.core.enums.config.BlocksToAvoid; import com.seibel.lod.core.enums.config.BufferRebuildTimes; import com.seibel.lod.core.enums.config.DistanceGenerationMode; +import com.seibel.lod.core.enums.config.DropoffQuality; import com.seibel.lod.core.enums.config.GenerationPriority; import com.seibel.lod.core.enums.config.GpuUploadMethod; import com.seibel.lod.core.enums.config.HorizontalQuality; @@ -140,6 +141,21 @@ public interface ILodConfigWrapperSingleton + " Highest Quality: " + HorizontalQuality.HIGH; HorizontalQuality getHorizontalQuality(); void setHorizontalQuality(HorizontalQuality newHorizontalQuality); + + DropoffQuality DROPOFF_QUALITY_DEFAULT = DropoffQuality.AUTO; + String DROPOFF_QUALITY_DESC = "" + + " This determines how lod level drop off will be done. \n" + + "\n" + + " " + DropoffQuality.SMOOTH_DROPOFF + ": \n" + + " The lod level is calculated for each point, making the drop off a smooth circle. \n" + + " " + DropoffQuality.PERFORMANCE_FOCUSED + ": \n" + + " One detail level for an entire region. Minimize CPU usage and \n" + + " improve terrain refresh delay, especially for high Lod render distance. \n" + + " " + DropoffQuality.AUTO + ": \n" + + " Use "+ DropoffQuality.SMOOTH_DROPOFF + " for less then 128 Lod render distance, \n" + + " or "+ DropoffQuality.PERFORMANCE_FOCUSED +" otherwise. \n"; + DropoffQuality getDropoffQuality(); + void setDropoffQuality(DropoffQuality newDropoffQuality); } interface IFogQuality From a402fa5f0b76d4a593dd565a04ebe711497107ff Mon Sep 17 00:00:00 2001 From: tom lee Date: Fri, 7 Jan 2022 19:08:18 +0800 Subject: [PATCH 14/20] Update lang file --- src/main/resources/assets/lod/lang/en_us.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/resources/assets/lod/lang/en_us.json b/src/main/resources/assets/lod/lang/en_us.json index 9bc8fcdc1..3d8510a94 100644 --- a/src/main/resources/assets/lod/lang/en_us.json +++ b/src/main/resources/assets/lod/lang/en_us.json @@ -16,6 +16,8 @@ "DistantHorizons.config.client.graphics.quality.horizontalScale.@tooltip": "This indicates how quickly fake chunks drop off in quality", "DistantHorizons.config.client.graphics.quality.horizontalQuality": "Horizontal quality", "DistantHorizons.config.client.graphics.quality.horizontalQuality.@tooltip": "This indicates the exponential base of the quadratic drop-off", + "DistantHorizons.config.client.graphics.quality.dropoffQuality": "Dropoff quality", + "DistantHorizons.config.client.graphics.quality.dropoffQuality.@tooltip": "This change how detail dropoff is done.", "DistantHorizons.config.client.graphics.fogQuality": "Fog options", "DistantHorizons.config.client.graphics.fogQuality.fogDistance": "Fog distance", "DistantHorizons.config.client.graphics.fogQuality.fogDistance.@tooltip": "At what distance should Fog be drawn on the fake chunks?", @@ -129,5 +131,8 @@ "DistantHorizons.config.enum.GpuUploadMethod.DATA": "Data", "DistantHorizons.config.enum.BufferRebuildTimes.FREQUENT": "Frequent", "DistantHorizons.config.enum.BufferRebuildTimes.NORMAL": "Normal", - "DistantHorizons.config.enum.BufferRebuildTimes.RARE": "Rare" + "DistantHorizons.config.enum.BufferRebuildTimes.RARE": "Rare", + "DistantHorizons.config.enum.DropoffQuality.AUTO": "Auto", + "DistantHorizons.config.enum.DropoffQuality.SMOOTH_DROPOFF": "Smooth dropoff", + "DistantHorizons.config.enum.DropoffQuality.PERFORMANCE_FOCUSED": "Performance focused" } From 8b761ca31af7d5df8372c73e03865771ac264de1 Mon Sep 17 00:00:00 2001 From: tom lee Date: Sat, 8 Jan 2022 22:51:23 +0800 Subject: [PATCH 15/20] Made IChunkWrapper also has the ability to return light data --- .../core/builders/lodBuilding/LodBuilder.java | 26 ++++++++++++------- .../chunk/IChunkWrapper.java | 4 +++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java b/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java index 8af36d0d7..bdb6f0370 100644 --- a/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java +++ b/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java @@ -399,7 +399,6 @@ public class LodBuilder // 1 means the lighting is a guess int isDefault = 0; - IWorldWrapper world = MC.getWrappedServerWorld(); int blockBrightness = chunk.getEmittedBrightness(x, y, z); // get the air block above or below this block @@ -408,12 +407,24 @@ public class LodBuilder else y++; + blockLight = chunk.getBlockLight(x, y, z); + if (hasSkyLight) + skyLight = chunk.getSkyLight(x, y, z); + else + skyLight = 0; + if (blockLight != -1 && skyLight != -1) { + blockLight = LodUtil.clamp(0, Math.max(blockLight, blockBrightness), DEFAULT_MAX_LIGHT); + return blockLight + (skyLight << 4) + (isDefault << 8); + } + + IWorldWrapper world = MC.getWrappedServerWorld(); if (world != null) { // server world sky light (always accurate) blockLight = world.getBlockLight(x,y,z); + if (topBlock && !hasCeiling && hasSkyLight) skyLight = DEFAULT_MAX_LIGHT; else @@ -429,11 +440,10 @@ public class LodBuilder // lets just take a guess if (y >= MC.getWrappedClientWorld().getSeaLevel() - 5) { - skyLight = 12; - isDefault = 1; + skyLight = 6; } else - skyLight = 0; + skyLight = 6; } } else @@ -442,8 +452,7 @@ public class LodBuilder if (world==null) { blockLight = 0; - skyLight = 12; - isDefault = 1; + skyLight = 6; } else { @@ -466,11 +475,10 @@ public class LodBuilder // lets just take a guess if (y >= MC.getWrappedClientWorld().getSeaLevel() - 5) { - skyLight = 12; - isDefault = 1; + skyLight = 6; } else - skyLight = 0; + skyLight = 6; } } } diff --git a/src/main/java/com/seibel/lod/core/wrapperInterfaces/chunk/IChunkWrapper.java b/src/main/java/com/seibel/lod/core/wrapperInterfaces/chunk/IChunkWrapper.java index 89f73401a..7cc71c5b3 100644 --- a/src/main/java/com/seibel/lod/core/wrapperInterfaces/chunk/IChunkWrapper.java +++ b/src/main/java/com/seibel/lod/core/wrapperInterfaces/chunk/IChunkWrapper.java @@ -55,4 +55,8 @@ public interface IChunkWrapper boolean isWaterLogged(int x, int y, int z); int getEmittedBrightness(int x, int y, int z); + + int getBlockLight(int x, int y, int z); + + int getSkyLight(int x, int y, int z); } From e5f5d33db95e96b5e73011ee576ea2c21dc27df8 Mon Sep 17 00:00:00 2001 From: tom lee Date: Sat, 8 Jan 2022 22:53:07 +0800 Subject: [PATCH 16/20] Add missing default --- .../lod/core/wrapperInterfaces/chunk/IChunkWrapper.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/seibel/lod/core/wrapperInterfaces/chunk/IChunkWrapper.java b/src/main/java/com/seibel/lod/core/wrapperInterfaces/chunk/IChunkWrapper.java index 7cc71c5b3..d37b3bcdf 100644 --- a/src/main/java/com/seibel/lod/core/wrapperInterfaces/chunk/IChunkWrapper.java +++ b/src/main/java/com/seibel/lod/core/wrapperInterfaces/chunk/IChunkWrapper.java @@ -56,7 +56,7 @@ public interface IChunkWrapper int getEmittedBrightness(int x, int y, int z); - int getBlockLight(int x, int y, int z); + default int getBlockLight(int x, int y, int z) {return -1;} - int getSkyLight(int x, int y, int z); + default int getSkyLight(int x, int y, int z) {return -1;} } From 943a2d5cada3235e93ea73eacce412cc887136ed Mon Sep 17 00:00:00 2001 From: tom lee Date: Sun, 9 Jan 2022 00:25:58 +0800 Subject: [PATCH 17/20] Remove default light hack. Try the first patch to get light work --- .../com/seibel/lod/core/api/ClientApi.java | 43 +++++++++++++++-- .../com/seibel/lod/core/api/EventApi.java | 20 ++++---- .../core/builders/lodBuilding/LodBuilder.java | 46 +++++++++++++++++++ .../world/IWorldWrapper.java | 4 ++ 4 files changed, 101 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/seibel/lod/core/api/ClientApi.java b/src/main/java/com/seibel/lod/core/api/ClientApi.java index eb338e824..7936ff45b 100644 --- a/src/main/java/com/seibel/lod/core/api/ClientApi.java +++ b/src/main/java/com/seibel/lod/core/api/ClientApi.java @@ -19,20 +19,28 @@ package com.seibel.lod.core.api; +import java.util.HashSet; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.seibel.lod.core.ModInfo; +import com.seibel.lod.core.enums.WorldType; +import com.seibel.lod.core.enums.config.DistanceGenerationMode; import com.seibel.lod.core.objects.lod.LodDimension; import com.seibel.lod.core.objects.math.Mat4f; import com.seibel.lod.core.render.GLProxy; import com.seibel.lod.core.render.LodRenderer; import com.seibel.lod.core.util.DetailDistanceUtil; import com.seibel.lod.core.util.SingletonHandler; +import com.seibel.lod.core.wrapperInterfaces.IWrapperFactory; +import com.seibel.lod.core.wrapperInterfaces.chunk.AbstractChunkPosWrapper; +import com.seibel.lod.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.lod.core.wrapperInterfaces.config.ILodConfigWrapperSingleton; import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftWrapper; import com.seibel.lod.core.wrapperInterfaces.minecraft.IProfilerWrapper; +import com.seibel.lod.core.wrapperInterfaces.world.IWorldWrapper; /** * This holds the methods that should be called @@ -52,6 +60,7 @@ public class ClientApi private static final IMinecraftWrapper MC = SingletonHandler.get(IMinecraftWrapper.class); private static final IMinecraftRenderWrapper MC_RENDER = SingletonHandler.get(IMinecraftRenderWrapper.class); private static final ILodConfigWrapperSingleton CONFIG = SingletonHandler.get(ILodConfigWrapperSingleton.class); + private static final IWrapperFactory WRAPPER_FACTORY = SingletonHandler.get(IWrapperFactory.class); private static final EventApi EVENT_API = EventApi.INSTANCE; /** @@ -69,14 +78,27 @@ public class ClientApi } + + private HashSet toBeLoaded = null; - + // TODO: Fix it + @Deprecated + public void clientChunkLoadEvent(IChunkWrapper chunk, IWorldWrapper world) + { + //EventApi.INSTANCE.chunkLoadEvent(chunk, world.getDimensionType()); + //ClientApi.LOGGER.info("ChunkLoadEvent called for "+ (world.getWorldType() == WorldType.ClientWorld ? "clientLevel" : "serverLevel"), new RuntimeException()); + + if (toBeLoaded == null) toBeLoaded = new HashSet(); + //toBeLoaded.add(WRAPPER_FACTORY.createChunkPos(chunk.getChunkPosX(), chunk.getChunkPosZ())); + } + + private HashSet lastFrame = new HashSet(); public void renderLods(Mat4f mcModelViewMatrix, Mat4f mcProjectionMatrix, float partialTicks) { // comment out when creating a release applyConfigOverrides(); - + // clear any out of date objects MC.clearFrameObjectCache(); @@ -85,8 +107,19 @@ public class ClientApi // only run the first time setup once if (!firstTimeSetupComplete) firstFrameSetup(); - - + + IWorldWrapper world = MC.getWrappedClientWorld(); + HashSet chunks = MC_RENDER.getVanillaRenderedChunks(); + if (chunks != null) { + for (AbstractChunkPosWrapper pos : chunks) { + if (lastFrame.contains(pos)) continue; + IChunkWrapper chunk = world.tryGetChunk(pos); + if (chunk == null) continue; + ApiShared.lodBuilder.generateLodNodeDirect(chunk, ApiShared.lodWorld, + world.getDimensionType(), DistanceGenerationMode.FULL, true); + } + } + lastFrame = chunks; if (!MC.playerExists() || ApiShared.lodWorld.getIsWorldNotLoaded()) return; @@ -98,6 +131,8 @@ public class ClientApi EVENT_API.viewDistanceChangedEvent(); EVENT_API.playerMoveEvent(lodDim); + + lodDim.cutRegionNodesAsync(MC.getPlayerBlockPos().getX(), MC.getPlayerBlockPos().getZ()); lodDim.expandOrLoadRegionsAsync(MC.getPlayerBlockPos().getX(), MC.getPlayerBlockPos().getZ()); diff --git a/src/main/java/com/seibel/lod/core/api/EventApi.java b/src/main/java/com/seibel/lod/core/api/EventApi.java index 57e831a2e..a965366d7 100644 --- a/src/main/java/com/seibel/lod/core/api/EventApi.java +++ b/src/main/java/com/seibel/lod/core/api/EventApi.java @@ -19,6 +19,8 @@ package com.seibel.lod.core.api; +import java.util.HashSet; + import org.lwjgl.glfw.GLFW; import com.seibel.lod.core.builders.lodBuilding.LodBuilder; @@ -34,6 +36,8 @@ import com.seibel.lod.core.util.DetailDistanceUtil; import com.seibel.lod.core.util.LodUtil; import com.seibel.lod.core.util.SingletonHandler; import com.seibel.lod.core.util.ThreadMapUtil; +import com.seibel.lod.core.wrapperInterfaces.IWrapperFactory; +import com.seibel.lod.core.wrapperInterfaces.chunk.AbstractChunkPosWrapper; import com.seibel.lod.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.lod.core.wrapperInterfaces.config.ILodConfigWrapperSingleton; import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftWrapper; @@ -66,8 +70,12 @@ public class EventApi { } - - + + @Deprecated + public void chunkLoadEvent(IChunkWrapper chunk, IDimensionTypeWrapper dimType) + { + //ApiShared.lodBuilder.generateLodNodeAsync(chunk, ApiShared.lodWorld, dimType, DistanceGenerationMode.FULL, true); + } //=============// @@ -94,11 +102,6 @@ public class EventApi // world events // //==============// - public void chunkLoadEvent(IChunkWrapper chunk, IDimensionTypeWrapper dimType) - { - ApiShared.lodBuilder.generateLodNodeAsync(chunk, ApiShared.lodWorld, dimType, DistanceGenerationMode.FULL, true); - } - public void worldSaveEvent() { ApiShared.lodWorld.saveAllDimensions(false); // Do an async save. @@ -109,7 +112,8 @@ public class EventApi /** This is also called when a new dimension loads */ public void worldLoadEvent(IWorldWrapper world) { - ClientApi.LOGGER.info("WorldLoadEvent called here for "+ (world.getWorldType() == WorldType.ClientWorld ? "clientLevel" : "serverLevel"), new RuntimeException()); + ClientApi.LOGGER.info("WorldLoadEvent called here for "+ (world.getWorldType() == WorldType.ClientWorld ? + "clientLevel" : "serverLevel"), new RuntimeException()); // Always ignore ServerWorld event if (world.getWorldType() == WorldType.ServerWorld) return; isCurrentlyOnSinglePlayerServer = MC.hasSinglePlayerServer(); diff --git a/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java b/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java index bdb6f0370..e1f1dc0b3 100644 --- a/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java +++ b/src/main/java/com/seibel/lod/core/builders/lodBuilding/LodBuilder.java @@ -142,6 +142,52 @@ public class LodBuilder }); lodGenThreadPool.execute(thread); } + + public void generateLodNodeDirect(IChunkWrapper chunk, LodWorld lodWorld, IDimensionTypeWrapper dim, DistanceGenerationMode generationMode, boolean override) + { + if (lodWorld == null || lodWorld.getIsWorldNotLoaded()) + return; + + // don't try to create an LOD object + // if for some reason we aren't + // given a valid chunk object + if (chunk == null) + return; + + //noinspection GrazieInspection + try + { + // we need a loaded client world in order to + // get the textures for blocks + if (MC.getWrappedClientWorld() == null) + return; + + // don't try to generate LODs if the user isn't in the world anymore + // (this happens a lot when the user leaves a world/server) + if (!MC.hasSinglePlayerServer() && !MC.connectedToServer()) + return; + + // make sure the dimension exists + LodDimension lodDim; + if (lodWorld.getLodDimension(dim) == null) + { + lodDim = new LodDimension(dim, lodWorld, defaultDimensionWidthInRegions); + lodWorld.addLodDimension(lodDim); + } + else + { + lodDim = lodWorld.getLodDimension(dim); + } + generateLodNodeFromChunk(lodDim, chunk, new LodBuilderConfig(generationMode), override); + } + catch (IllegalArgumentException | NullPointerException e) + { + e.printStackTrace(); + // if the world changes while LODs are being generated + // they will throw errors as they try to access things that no longer + // exist. + } + } /** * Creates a LodNode for a chunk in the given world. diff --git a/src/main/java/com/seibel/lod/core/wrapperInterfaces/world/IWorldWrapper.java b/src/main/java/com/seibel/lod/core/wrapperInterfaces/world/IWorldWrapper.java index ba6214e62..6726d1a56 100644 --- a/src/main/java/com/seibel/lod/core/wrapperInterfaces/world/IWorldWrapper.java +++ b/src/main/java/com/seibel/lod/core/wrapperInterfaces/world/IWorldWrapper.java @@ -23,6 +23,8 @@ import java.io.File; import com.seibel.lod.core.enums.WorldType; import com.seibel.lod.core.wrapperInterfaces.block.AbstractBlockPosWrapper; +import com.seibel.lod.core.wrapperInterfaces.chunk.AbstractChunkPosWrapper; +import com.seibel.lod.core.wrapperInterfaces.chunk.IChunkWrapper; /** * Can be either a Server world or a Client world. @@ -60,5 +62,7 @@ public interface IWorldWrapper /** @throws UnsupportedOperationException if the WorldWrapper isn't for a ServerWorld */ File getSaveFolder() throws UnsupportedOperationException; + default IChunkWrapper tryGetChunk(AbstractChunkPosWrapper pos) {return null;} + } From d1e1970c180e3b0e82d0800782b2ea9e806eb5fc Mon Sep 17 00:00:00 2001 From: tom lee Date: Sun, 9 Jan 2022 14:56:49 +0800 Subject: [PATCH 18/20] Added ModAccessor sturcture for adding mod compats --- .../com/seibel/lod/core/api/ClientApi.java | 1 - .../com/seibel/lod/core/api/EventApi.java | 5 -- .../seibel/lod/core/api/ModAccessorApi.java | 53 +++++++++++++++++++ .../com/seibel/lod/core/util/LodUtil.java | 2 +- .../minecraft/IMinecraftRenderWrapper.java | 16 ++---- .../modAccessor/IModAccessor.java | 5 ++ .../modAccessor/ISodiumAccessor.java | 9 ++++ 7 files changed, 72 insertions(+), 19 deletions(-) create mode 100644 src/main/java/com/seibel/lod/core/api/ModAccessorApi.java create mode 100644 src/main/java/com/seibel/lod/core/wrapperInterfaces/modAccessor/IModAccessor.java create mode 100644 src/main/java/com/seibel/lod/core/wrapperInterfaces/modAccessor/ISodiumAccessor.java diff --git a/src/main/java/com/seibel/lod/core/api/ClientApi.java b/src/main/java/com/seibel/lod/core/api/ClientApi.java index 7936ff45b..2fc973d4e 100644 --- a/src/main/java/com/seibel/lod/core/api/ClientApi.java +++ b/src/main/java/com/seibel/lod/core/api/ClientApi.java @@ -60,7 +60,6 @@ public class ClientApi private static final IMinecraftWrapper MC = SingletonHandler.get(IMinecraftWrapper.class); private static final IMinecraftRenderWrapper MC_RENDER = SingletonHandler.get(IMinecraftRenderWrapper.class); private static final ILodConfigWrapperSingleton CONFIG = SingletonHandler.get(ILodConfigWrapperSingleton.class); - private static final IWrapperFactory WRAPPER_FACTORY = SingletonHandler.get(IWrapperFactory.class); private static final EventApi EVENT_API = EventApi.INSTANCE; /** diff --git a/src/main/java/com/seibel/lod/core/api/EventApi.java b/src/main/java/com/seibel/lod/core/api/EventApi.java index a965366d7..12a04ed66 100644 --- a/src/main/java/com/seibel/lod/core/api/EventApi.java +++ b/src/main/java/com/seibel/lod/core/api/EventApi.java @@ -19,14 +19,11 @@ package com.seibel.lod.core.api; -import java.util.HashSet; - import org.lwjgl.glfw.GLFW; import com.seibel.lod.core.builders.lodBuilding.LodBuilder; import com.seibel.lod.core.builders.worldGeneration.LodWorldGenerator; import com.seibel.lod.core.enums.WorldType; -import com.seibel.lod.core.enums.config.DistanceGenerationMode; import com.seibel.lod.core.objects.lod.LodDimension; import com.seibel.lod.core.objects.lod.RegionPos; import com.seibel.lod.core.render.GLProxy; @@ -36,8 +33,6 @@ import com.seibel.lod.core.util.DetailDistanceUtil; import com.seibel.lod.core.util.LodUtil; import com.seibel.lod.core.util.SingletonHandler; import com.seibel.lod.core.util.ThreadMapUtil; -import com.seibel.lod.core.wrapperInterfaces.IWrapperFactory; -import com.seibel.lod.core.wrapperInterfaces.chunk.AbstractChunkPosWrapper; import com.seibel.lod.core.wrapperInterfaces.chunk.IChunkWrapper; import com.seibel.lod.core.wrapperInterfaces.config.ILodConfigWrapperSingleton; import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftWrapper; diff --git a/src/main/java/com/seibel/lod/core/api/ModAccessorApi.java b/src/main/java/com/seibel/lod/core/api/ModAccessorApi.java new file mode 100644 index 000000000..adc1ec942 --- /dev/null +++ b/src/main/java/com/seibel/lod/core/api/ModAccessorApi.java @@ -0,0 +1,53 @@ +package com.seibel.lod.core.api; + +import java.util.HashMap; +import java.util.Map; + +import com.seibel.lod.core.wrapperInterfaces.modAccessor.IModAccessor; + +/** + * This class takes care of dependency injection + * for mods (for mod compatibility support). + * + * (Basically the same as SingletonHandler, except + * it can return null which means that mod aren't + * loaded in the game, or it haven't been implemented + * for that build.) + */ +public class ModAccessorApi { + private static final Map, IModAccessor> singletons = new HashMap, IModAccessor>(); + + public static void bind(Class interfaceClass, IModAccessor modAccessor) throws IllegalStateException + { + // make sure we haven't already bound this singleton + if (singletons.containsKey(interfaceClass)) + { + throw new IllegalStateException("The modAccessor [" + interfaceClass.getSimpleName() + "] has already been bound."); + } + + // make sure the given singleton implements the interface + boolean singletonImplementsInterface = false; + for (Class singletonInterface : modAccessor.getClass().getInterfaces()) + { + if (singletonInterface.equals(interfaceClass)) + { + singletonImplementsInterface = true; + break; + } + } + if (!singletonImplementsInterface) + { + throw new IllegalStateException("The singleton [" + interfaceClass.getSimpleName() + "] doesn't implement the interface [" + interfaceClass.getSimpleName() + "]."); + } + ClientApi.LOGGER.info("DistantHorizon: Registored mod comatibility accessor for "+modAccessor.getModName()); + singletons.put(interfaceClass, modAccessor); + } + + @SuppressWarnings("unchecked") + public static T get(Class objectClass) throws ClassCastException + { + IModAccessor modAccessor = singletons.get(objectClass); + return modAccessor==null ? null : (T) modAccessor; + } + +} diff --git a/src/main/java/com/seibel/lod/core/util/LodUtil.java b/src/main/java/com/seibel/lod/core/util/LodUtil.java index f301aa520..e11989830 100644 --- a/src/main/java/com/seibel/lod/core/util/LodUtil.java +++ b/src/main/java/com/seibel/lod/core/util/LodUtil.java @@ -364,7 +364,7 @@ public class LodUtil // get the chunks that are going to be rendered by Minecraft - HashSet posToSkip = REFLECTION_HANDLER.sodiumPresent() ? MC_RENDER.getSodiumRenderedChunks() : MC_RENDER.getVanillaRenderedChunks(); + HashSet posToSkip = MC_RENDER.getVanillaRenderedChunks(); // remove everything outside the skipRadius, diff --git a/src/main/java/com/seibel/lod/core/wrapperInterfaces/minecraft/IMinecraftRenderWrapper.java b/src/main/java/com/seibel/lod/core/wrapperInterfaces/minecraft/IMinecraftRenderWrapper.java index 03722b04b..6ef47b760 100644 --- a/src/main/java/com/seibel/lod/core/wrapperInterfaces/minecraft/IMinecraftRenderWrapper.java +++ b/src/main/java/com/seibel/lod/core/wrapperInterfaces/minecraft/IMinecraftRenderWrapper.java @@ -22,6 +22,7 @@ package com.seibel.lod.core.wrapperInterfaces.minecraft; import java.awt.Color; import java.util.HashSet; +import com.seibel.lod.core.api.ModAccessorApi; import com.seibel.lod.core.objects.math.Mat4f; import com.seibel.lod.core.objects.math.Vec3d; import com.seibel.lod.core.objects.math.Vec3f; @@ -29,6 +30,7 @@ import com.seibel.lod.core.util.SingletonHandler; import com.seibel.lod.core.wrapperInterfaces.IWrapperFactory; import com.seibel.lod.core.wrapperInterfaces.block.AbstractBlockPosWrapper; import com.seibel.lod.core.wrapperInterfaces.chunk.AbstractChunkPosWrapper; +import com.seibel.lod.core.wrapperInterfaces.modAccessor.ISodiumAccessor; /** * Contains everything related to @@ -77,18 +79,8 @@ public interface IMinecraftRenderWrapper */ public default HashSet getVanillaRenderedChunks() { - return getMaximumRenderedChunks(); - } - - /** - * This method returns the ChunkPos of every chunk that - * Sodium is going to render this frame. - *
- * If not implemented this calls {@link #getMaximumRenderedChunks()}. - */ - public default HashSet getSodiumRenderedChunks() - { - return getMaximumRenderedChunks(); + ISodiumAccessor sodium = ModAccessorApi.get(ISodiumAccessor.class); + return sodium==null ? getMaximumRenderedChunks() : sodium.getNormalRenderedChunks(); } /** diff --git a/src/main/java/com/seibel/lod/core/wrapperInterfaces/modAccessor/IModAccessor.java b/src/main/java/com/seibel/lod/core/wrapperInterfaces/modAccessor/IModAccessor.java new file mode 100644 index 000000000..dbf0b878e --- /dev/null +++ b/src/main/java/com/seibel/lod/core/wrapperInterfaces/modAccessor/IModAccessor.java @@ -0,0 +1,5 @@ +package com.seibel.lod.core.wrapperInterfaces.modAccessor; + +public abstract interface IModAccessor { + String getModName(); +} diff --git a/src/main/java/com/seibel/lod/core/wrapperInterfaces/modAccessor/ISodiumAccessor.java b/src/main/java/com/seibel/lod/core/wrapperInterfaces/modAccessor/ISodiumAccessor.java new file mode 100644 index 000000000..d2335c8e2 --- /dev/null +++ b/src/main/java/com/seibel/lod/core/wrapperInterfaces/modAccessor/ISodiumAccessor.java @@ -0,0 +1,9 @@ +package com.seibel.lod.core.wrapperInterfaces.modAccessor; + +import java.util.HashSet; + +import com.seibel.lod.core.wrapperInterfaces.chunk.AbstractChunkPosWrapper; + +public interface ISodiumAccessor extends IModAccessor { + HashSet getNormalRenderedChunks(); +} From ee2c6e2a06d235094a45c19ff877fd794f4c95a6 Mon Sep 17 00:00:00 2001 From: coolGi2007 Date: Mon, 10 Jan 2022 04:50:05 +0000 Subject: [PATCH 19/20] Added a cloud warning to the lang --- src/main/resources/assets/lod/lang/en_us.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/resources/assets/lod/lang/en_us.json b/src/main/resources/assets/lod/lang/en_us.json index 3d8510a94..d80c49345 100644 --- a/src/main/resources/assets/lod/lang/en_us.json +++ b/src/main/resources/assets/lod/lang/en_us.json @@ -28,6 +28,7 @@ "DistantHorizons.config.client.graphics.fogQuality.disableVanillaFog": "Disable vanilla fog", "DistantHorizons.config.client.graphics.fogQuality.disableVanillaFog.@tooltip": "If true disable Minecraft's fog. \nMay cause issues with other mods that edit fog. \nMay cause errors with other fog editing mods", "DistantHorizons.config.client.graphics.cloudQuality": "Cloud options", + "DistantHorizons.config.client.graphics.cloudQuality.cloudWarning": "§l§nWARNING:§r§n Clouds are still experimental", "DistantHorizons.config.client.graphics.cloudQuality.customClouds": "Custom cloud", "DistantHorizons.config.client.graphics.cloudQuality.customClouds.@tooltip": "Do you want to use a custom way or rendering the clouds. \nIf you turn this off then the other settings here wont do anything \nThis will also make the clouds more dynamic", "DistantHorizons.config.client.graphics.cloudQuality.fabulousClouds": "Fabulous clouds", From efd1d67f3f8c8799983c5f5a6e473f84d07ae352 Mon Sep 17 00:00:00 2001 From: tom lee Date: Mon, 10 Jan 2022 22:03:54 +0800 Subject: [PATCH 20/20] Render: Fixed mistakes and added more GridList stuff --- .../seibel/lod/core/render/LodRenderer.java | 31 +-- .../lod/core/util/BooleanMovableGridList.java | 248 ++++++++++++++++++ .../seibel/lod/core/util/MovableGridList.java | 9 +- 3 files changed, 268 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/seibel/lod/core/util/BooleanMovableGridList.java diff --git a/src/main/java/com/seibel/lod/core/render/LodRenderer.java b/src/main/java/com/seibel/lod/core/render/LodRenderer.java index b80ed2bac..233737f92 100644 --- a/src/main/java/com/seibel/lod/core/render/LodRenderer.java +++ b/src/main/java/com/seibel/lod/core/render/LodRenderer.java @@ -49,6 +49,7 @@ import com.seibel.lod.core.wrapperInterfaces.config.ILodConfigWrapperSingleton; import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftRenderWrapper; import com.seibel.lod.core.wrapperInterfaces.minecraft.IMinecraftWrapper; import com.seibel.lod.core.wrapperInterfaces.minecraft.IProfilerWrapper; +import com.seibel.lod.core.wrapperInterfaces.world.IWorldWrapper; /** * This is where all the magic happens.
@@ -487,12 +488,13 @@ public class LodRenderer } // returns whether anything changed - private boolean updateVanillaRenderedChunks(LodDimension lodDim, boolean recreateChunks) { - short chunkRenderDistance = (short) MC_RENDER.getRenderDistance(); + private boolean updateVanillaRenderedChunks(LodDimension lodDim) { + int chunkRenderDistance = MC_RENDER.getRenderDistance()+2; int chunkX = Math.floorDiv(lastUpdatedPos.getX(), 16); int chunkZ = Math.floorDiv(lastUpdatedPos.getZ(), 16); // if the player is high enough, draw all LODs - if (lastUpdatedPos.getY() > 256) { + IWorldWrapper world = MC.getWrappedClientWorld(); + if (lastUpdatedPos.getY() > world.getHeight()-world.getMinHeight()) { vanillaRenderedChunks = new MovableGridList( chunkRenderDistance, chunkX, chunkZ); return true; @@ -500,14 +502,12 @@ public class LodRenderer MovableGridList chunkList; boolean anyChanged = false; - if (recreateChunks || vanillaRenderedChunks.gridCentreToEdge != chunkRenderDistance) { + if (vanillaRenderedChunks == null || vanillaRenderedChunks.gridCentreToEdge != chunkRenderDistance || + vanillaRenderedChunks.getCenterX()!=chunkX || vanillaRenderedChunks.getCenterY()!=chunkZ) { chunkList = new MovableGridList(chunkRenderDistance, chunkX, chunkZ); anyChanged = true; } else { - // anyChanged = vanillaRenderedChunks.move(chunkX, chunkZ); - // chunkList = vanillaRenderedChunks; - chunkList = new MovableGridList(chunkRenderDistance, chunkX, chunkZ); - anyChanged = true; + chunkList = vanillaRenderedChunks; } LagSpikeCatcher getChunks = new LagSpikeCatcher(); @@ -534,7 +534,6 @@ public class LodRenderer long newTime = System.currentTimeMillis(); AbstractBlockPosWrapper newPos = MC.getPlayerBlockPos(); boolean shouldUpdateChunks = false; - boolean posUpdated = false; boolean tryPartialGen = false; boolean tryFullGen = false; @@ -559,9 +558,7 @@ public class LodRenderer || Math.abs(newPos.getX() - lastUpdatedPos.getX()) > CONFIG.client().advanced().buffers().getRebuildTimes().playerMoveDistance*16 || Math.abs(newPos.getZ() - lastUpdatedPos.getZ()) > CONFIG.client().advanced().buffers().getRebuildTimes().playerMoveDistance*16) { - tryPartialGen = true; - lastUpdatedPos = newPos; - posUpdated = true; + shouldUpdateChunks = true; } prevPlayerPosTime = newTime; } @@ -580,14 +577,10 @@ public class LodRenderer prevChunkTime = newTime; } - - if (tryFullGen && !posUpdated) { - lastUpdatedPos = newPos; - posUpdated = true; - } - shouldUpdateChunks |= posUpdated; + shouldUpdateChunks |= tryFullGen; if (shouldUpdateChunks) { - tryPartialGen |= updateVanillaRenderedChunks(lodDim, posUpdated); + lastUpdatedPos = newPos; + tryPartialGen |= updateVanillaRenderedChunks(lodDim); } if (tryFullGen) { diff --git a/src/main/java/com/seibel/lod/core/util/BooleanMovableGridList.java b/src/main/java/com/seibel/lod/core/util/BooleanMovableGridList.java new file mode 100644 index 000000000..2cdb5058f --- /dev/null +++ b/src/main/java/com/seibel/lod/core/util/BooleanMovableGridList.java @@ -0,0 +1,248 @@ +package com.seibel.lod.core.util; + +/*Layout: + * 0,1,2, + * 3,4,5, + * 6,7,8 + */ + +public class BooleanMovableGridList { + + private int centerX; + private int centerY; + + public final int gridCentreToEdge; + public final int gridSize; + private boolean[] b; + + public BooleanMovableGridList(int gridCentreToEdge, int centerX, int centerY) { + gridSize = gridCentreToEdge * 2 + 1; + this.gridCentreToEdge = gridCentreToEdge; + this.centerX = centerX; + this.centerY = centerY; + clear(); + } + + public void clear() { + b = new boolean[gridSize*gridSize]; + } + + public int getCenterX() {return centerX;} + public int getCenterY() {return centerY;} + + private void assertIndex(int ix, int iy) { + if (ix<0 || ix>=gridSize || iy<0 || iy>=gridSize) + throw new IndexOutOfBoundsException("BooleanMovableGridList index position out of bound"); + } + + public boolean isInBound(int x, int y) { + x = x-centerX+gridCentreToEdge; + y = y-centerY+gridCentreToEdge; + return !(x<0 || x>=gridSize || y<0 || y>=gridSize); + } + + // return onFail if x,y is outside of the grid + public boolean get(int x, int y) { + x = x-centerX+gridCentreToEdge; + y = y-centerY+gridCentreToEdge; + return _getDirect(x,y); + } + + // return false if x,y is outside of the grid + public void set(int x, int y, boolean t) { + x = x-centerX+gridCentreToEdge; + y = y-centerY+gridCentreToEdge; + _setDirect(x,y,t); + } + + // return onFail if x,y is outside of the grid + // Otherwise, return the new value (for chaining) + public boolean setAndGet(int x, int y, boolean t) { + x = x-centerX+gridCentreToEdge; + y = y-centerY+gridCentreToEdge; + _setDirect(x,y,t); + return t; + } + // return null if x,y is outside of the grid + // Otherwise, return the old value + public boolean swap(int x, int y, boolean t, boolean onFail) { + x = x-centerX+gridCentreToEdge; + y = y-centerY+gridCentreToEdge; + return _swapDirect(x,y, t); + } + + private final boolean _getDirect(int x, int y) { + assertIndex(x,y); + return b[x + y * gridSize]; + } + private final void _setDirect(int x, int y, boolean t) { + assertIndex(x,y); + b[x + y * gridSize] = t; + } + private final boolean _swapDirect(int x, int y, boolean t) { + assertIndex(x,y); + boolean r = b[x + y * gridSize]; + b[x + y * gridSize] = t; + return r; + } + + interface BoolTransformer { + boolean transform(boolean oldValue, int x, int y); + } + + // Transform the list via the function. The data can still be accessed + // inside the function, and the returned value will not be applied + // until all elements have done the transform. + public void twoStageTransform(BoolTransformer transformer) { + boolean[] nb = new boolean[b.length]; + int i=0; + for (int y=0; y { + if (v!=t) return v; + boolean r = false; + r |= (isInBound(x-1,y) ? get(x-1,y)==!t : false); + r |= (isInBound(x,y-1) ? get(x,y-1)==!t : false); + r |= (isInBound(x+1,y) ? get(x+1,y)==!t : false); + r |= (isInBound(x,y+1) ? get(x,y+1)==!t : false); + return r ? !t : t; + }; + twoStageTransform(tran); + } + public void flipBorderCorner(boolean valueToBeFlipped) { + boolean t = valueToBeFlipped; + BoolTransformer tran = (v, x, y) -> { + if (v!=t) return v; + boolean r = false; + r |= (isInBound(x-1,y) ? get(x-1,y)==!t : false); + r |= (isInBound(x,y-1) ? get(x,y-1)==!t : false); + r |= (isInBound(x+1,y) ? get(x+1,y)==!t : false); + r |= (isInBound(x,y+1) ? get(x,y+1)==!t : false); + r |= (isInBound(x-1,y-1) ? get(x-1,y-1)==!t : false); + r |= (isInBound(x+1,y-1) ? get(x+1,y-1)==!t : false); + r |= (isInBound(x+1,y+1) ? get(x+1,y+1)==!t : false); + r |= (isInBound(x-1,y+1) ? get(x-1,y+1)==!t : false); + return r ? !t : t; + }; + twoStageTransform(tran); + } + public void flipBorderCorner(boolean valueToBeFlipped, int range) { + boolean t = valueToBeFlipped; + BoolTransformer tran = (v, x, y) -> { + if (v!=t) return v; + boolean r = false; + for (int dx=-range;dx<=range;dx++) + for (int dy=-range;dy<=range;dy++) + r |= (isInBound(x+dx,y+dy) ? get(x+dx,y+dy)==!t : false); + return r ? !t : t; + }; + twoStageTransform(tran); + } + + + // Return false if haven't changed. Return true if it did + public boolean move(int newCenterX, int newCenterY) { + return move(newCenterX, newCenterY, false); + } + + // Return false if haven't changed. Return true if it did + public boolean move(int newCenterX, int newCenterY, boolean value) { + if (centerX == newCenterX && centerY == newCenterY) return false; + int deltaX = newCenterX - centerX; + int deltaY = newCenterY - centerY; + + // if the x or z offset is equal to or greater than + // the total width, just delete the current data + // and update the centerX and/or centerZ + if (Math.abs(deltaX) >= gridSize || Math.abs(deltaY) >= gridSize) + { + clear(); + // update the new center + centerX = newCenterX; + centerY = newCenterY; + return true; + } + centerX = newCenterX; + centerY = newCenterY; + + // X + if (deltaX >= 0 && deltaY >= 0) + { + // move everything over to the left-up (as the center moves to the right-down) + for (int x = 0; x < gridSize; x++) + { + for (int y = 0; y < gridSize; y++) + { + _setDirect(x, y, _getDirect(x+deltaX, y+deltaY)); + } + } + } + else if (deltaX < 0 && deltaY >= 0) + { + // move everything over to the right-up (as the center moves to the left-down) + for (int x = gridSize - 1; x >= 0; x--) + { + for (int y = 0; y < gridSize; y++) + { + _setDirect(x, y, _getDirect(x+deltaX, y+deltaY)); + } + } + } + else if (deltaX >= 0 && deltaY < 0) + { + // move everything over to the left-down (as the center moves to the right-up) + for (int x = 0; x < gridSize; x++) + { + for (int y = gridSize - 1; y >= 0; y--) + { + _setDirect(x, y, _getDirect(x+deltaX, y+deltaY)); + } + } + } + else //if (deltaX < 0 && deltaY < 0) + { + // move everything over to the right-down (as the center moves to the left-up) + for (int x = gridSize - 1; x >= 0; x--) + { + for (int y = gridSize - 1; y >= 0; y--) + { + _setDirect(x, y, _getDirect(x+deltaX, y+deltaY)); + } + } + } + return true; + } + + + @Override + public String toString() { + return "MovableGridList[" + centerX + "," + centerY + "] " + gridSize + "*" + gridSize + "[" + b.length + "]"; + } + + public String toDetailString() { + StringBuilder str = new StringBuilder("\n"); + int i = 0; + str.append(toString()); + str.append("\n"); + for (boolean t : b) { + str.append(t ? "#" : "."); + i++; + if (i % gridSize == 0) { + str.append("\n"); + } else { + //str.append(", "); + } + } + return str.toString(); + } +} diff --git a/src/main/java/com/seibel/lod/core/util/MovableGridList.java b/src/main/java/com/seibel/lod/core/util/MovableGridList.java index d620321da..79d6093f2 100644 --- a/src/main/java/com/seibel/lod/core/util/MovableGridList.java +++ b/src/main/java/com/seibel/lod/core/util/MovableGridList.java @@ -66,6 +66,13 @@ public class MovableGridList extends ArrayList implements List { return _getDirect(x,y); } + // return false if x,y is outside of the grid + public boolean set(int x, int y, T t) { + x = x-centerX+gridCentreToEdge; + y = y-centerY+gridCentreToEdge; + return _setDirect(x,y, t); + } + // return null if x,y is outside of the grid // Otherwise, return the new value (for chaining) public T setAndGet(int x, int y, T t) { @@ -244,7 +251,7 @@ public class MovableGridList extends ArrayList implements List { } } } - + // TODO: This is unused but may be useful later on. /*